Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Sources/App/BarcodeScanner/Camera/BarcodeScannerCamera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ final class BarcodeScannerCamera: NSObject {
).devices
}

public var screenSize: CGSize? {
didSet {
// Prevent unecessary updates
guard let screenSize else { return }
// Calculate normalized rectOfInterest for AVFoundation
let cameraSquareSize = BarcodeScannerView.cameraSquareSize
let x = (screenSize.width - cameraSquareSize) / 2.0
let y = (screenSize.height - cameraSquareSize) / 2.0
// AVFoundation expects (x, y, width, height) in normalized coordinates (0-1),
// and (0,0) is top-left of the video in portrait orientation
let normalizedX = y / screenSize.height
let normalizedY = x / screenSize.width
let normalizedWidth = cameraSquareSize / screenSize.height
let normalizedHeight = cameraSquareSize / screenSize.width
captureSession.beginConfiguration()
metadataOutput.rectOfInterest = CGRect(
x: normalizedX,
y: normalizedY,
width: normalizedWidth,
height: normalizedHeight
)
captureSession.commitConfiguration()
}
}

private var availableCaptureDevices: [AVCaptureDevice] {
allBackCaptureDevices
.filter(\.isConnected)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import SwiftUI
struct BarcodeScannerCameraView: View {
@StateObject private var model: BarcodeScannerDataModel
private let shouldStartCamera: Bool
private let screenSize: CGSize

init(model: BarcodeScannerDataModel, shouldStartCamera: Bool = true) {
init(screenSize: CGSize, model: BarcodeScannerDataModel, shouldStartCamera: Bool = true) {
self._model = .init(wrappedValue: model)
self.shouldStartCamera = shouldStartCamera
self.screenSize = screenSize

// If camera shouldn't be started, no need to forward screen size for rect of interest
if shouldStartCamera {
model.camera.screenSize = screenSize
}
}

var body: some View {
Expand Down
73 changes: 45 additions & 28 deletions Sources/App/BarcodeScanner/Camera/BarcodeScannerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import Shared
import SwiftUI

struct BarcodeScannerView: View {
static let cameraSquareSize: CGFloat = 320

@Environment(\.dismiss) private var dismiss
@StateObject private var viewModel: BarcodeScannerViewModel
// Use single data model so both camera previews use same camera stream
@State private var cameraDataModel = BarcodeScannerDataModel()
private let cameraSquareSize: CGFloat = 320
@StateObject private var cameraDataModel = BarcodeScannerDataModel()
private let flashlightIcon = MaterialDesignIcons.flashlightIcon.image(
ofSize: .init(width: 24, height: 24),
color: .white
Expand Down Expand Up @@ -50,22 +51,20 @@ struct BarcodeScannerView: View {

private var topInformation: some View {
VStack(spacing: 8) {
Button(action: {
viewModel.aborted(.canceled)
dismiss()
}, label: {
Image(systemName: "xmark")
.font(.title2)
.foregroundColor(.white.opacity(0.8))
.frame(maxWidth: .infinity, alignment: .leading)
})
.accessibilityHint(.init(L10n.closeLabel))
HStack {
Spacer()
CloseButton(tint: .white, size: .medium) {
viewModel.aborted(.canceled)
dismiss()
}
.accessibilityHint(.init(L10n.closeLabel))
}
Group {
Text(title)
.padding(.top)
.font(.title2)
.font(DesignSystem.Font.title2.bold())
Text(description)
.font(.subheadline)
.font(DesignSystem.Font.subheadline)
}
.foregroundColor(.white)

Expand All @@ -85,39 +84,57 @@ struct BarcodeScannerView: View {
}

private var cameraBackground: some View {
BarcodeScannerCameraView(model: cameraDataModel)
.ignoresSafeArea()
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
.overlay {
Color.black.opacity(0.8)
}
GeometryReader { proxy in
BarcodeScannerCameraView(screenSize: proxy.size, model: cameraDataModel)
.ignoresSafeArea()
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
.overlay {
Color.black.opacity(0.8)
}
}
}

private var cameraSquare: some View {
BarcodeScannerCameraView(model: cameraDataModel, shouldStartCamera: false)
// No size needs to be provided here since it wont forward to rectOfInterest
BarcodeScannerCameraView(screenSize: .zero, model: cameraDataModel, shouldStartCamera: false)
.ignoresSafeArea()
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
.mask {
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
.frame(width: cameraSquareSize, height: cameraSquareSize)
.frame(
width: BarcodeScannerView.cameraSquareSize,
height: BarcodeScannerView.cameraSquareSize
)
}
.overlay {
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
.stroke(Color.clear, lineWidth: 1)
.frame(
width: BarcodeScannerView.cameraSquareSize,
height: BarcodeScannerView.cameraSquareSize
)
}
.shadow(color: .haPrimary.opacity(0.8), radius: 10, x: 0, y: 0)
.overlay {
ZStack(alignment: .bottomTrailing) {
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
.stroke(Color.blue, lineWidth: 1)
.frame(width: cameraSquareSize, height: cameraSquareSize)
VStack {
Spacer()
Button(action: {
cameraDataModel.toggleFlashlight()
}, label: {
Image(uiImage: flashlightIcon)
.padding()
.background(Color(uiColor: .init(hex: "#384956")))
.mask(Circle())
.offset(x: -22, y: -22)
.padding([.trailing, .bottom], DesignSystem.Spaces.two)
})
.frame(maxWidth: .infinity, alignment: .trailing)
}
.frame(
width: BarcodeScannerView.cameraSquareSize,
height: BarcodeScannerView.cameraSquareSize
)
}
}
}
Expand Down
28 changes: 24 additions & 4 deletions Sources/App/WebView/WebViewExternalMessageHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class WebViewExternalMessageHandler {
self.improvManager = improvManager
}

func handleExternalMessage(_ dictionary: [String: Any]) {
@MainActor func handleExternalMessage(_ dictionary: [String: Any]) {
guard let webViewController else {
Current.Log.error("WebViewExternalMessageHandler has nil webViewController")
return
Expand Down Expand Up @@ -116,9 +116,7 @@ final class WebViewExternalMessageHandler {
}
case .barCodeScannerNotify:
guard let message = incomingMessage.Payload?["message"] as? String else { return }
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.addAction(.init(title: L10n.okLabel, style: .default))
webViewController.presentAlertController(controller: alert, animated: true)
presentBarcodeScannerMessage(message: message)
case .threadStoreCredentialInAppleKeychain:
guard let macExtendedAddress = incomingMessage.Payload?["mac_extended_address"] as? String,
let activeOperationalDataset = incomingMessage.Payload?["active_operational_dataset"] as? String else { return }
Expand Down Expand Up @@ -324,6 +322,28 @@ final class WebViewExternalMessageHandler {
}
}

@MainActor
private func presentBarcodeScannerMessage(message: String) {
var config = SwiftMessages.Config()
config.dimMode = .none
config.presentationStyle = .bottom
config.duration = .seconds(seconds: 3)
let view = MessageView.viewFromNib(layout: .cardView)
view.configureContent(
title: nil,
body: message,
iconImage: nil,
iconText: nil,
buttonImage: nil,
buttonTitle: nil,
buttonTapHandler: { _ in
SwiftMessages.hide()
}
)
view.id = "BarcodeScannerMessage"
SwiftMessages.show(config: config, view: view)
}

private func cleanPreferredThreadCredentials() {
Current.settingsStore.matterLastPreferredNetWorkMacExtendedAddress = nil
Current.settingsStore.matterLastPreferredNetWorkActiveOperationalDataset = nil
Expand Down
21 changes: 11 additions & 10 deletions Tests/App/WebView/WebViewExternalMessageHandlerTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@testable import HomeAssistant
import Improv_iOS
import SwiftMessages
import SwiftUI
import XCTest

Expand All @@ -15,7 +16,7 @@ final class WebViewExternalMessageHandlerTests: XCTestCase {
sut.webViewController = mockWebViewController
}

func testHandleExternalMessageConfigScreenShowShowSettings() {
@MainActor func testHandleExternalMessageConfigScreenShowShowSettings() {
let dictionary: [String: Any] = [
"id": 1,
"message": "",
Expand All @@ -31,7 +32,7 @@ final class WebViewExternalMessageHandlerTests: XCTestCase {
)
}

func testHandleExternalMessageThemeUpdateNotifyThemeColors() {
@MainActor func testHandleExternalMessageThemeUpdateNotifyThemeColors() {
let dictionary: [String: Any] = [
"id": 1,
"message": "",
Expand All @@ -43,7 +44,7 @@ final class WebViewExternalMessageHandlerTests: XCTestCase {
XCTAssertEqual(mockWebViewController.lastEvaluatedJavaScriptScript, "notifyThemeColors()")
}

func testHandleExternalMessageBarCodeScanPresentsScanner() {
@MainActor func testHandleExternalMessageBarCodeScanPresentsScanner() {
let dictionary: [String: Any] = [
"id": 1,
"message": "",
Expand All @@ -59,7 +60,7 @@ final class WebViewExternalMessageHandlerTests: XCTestCase {
XCTAssertTrue(mockWebViewController.overlayedController is BarcodeScannerHostingController)
}

func testHandleExternalMessageBarCodeCloseClosesScanner() {
@MainActor func testHandleExternalMessageBarCodeCloseClosesScanner() {
let dictionary: [String: Any] = [
"id": 1,
"message": "",
Expand All @@ -86,7 +87,7 @@ final class WebViewExternalMessageHandlerTests: XCTestCase {
XCTAssertTrue(mockWebViewController.dismissControllerAboveOverlayControllerCalled)
}

func testHandleExternalMessageBarCodeNotifyNotifies() {
@MainActor func testHandleExternalMessageBarCodeNotifyNotifies() {
let dictionary: [String: Any] = [
"id": 1,
"message": "",
Expand All @@ -111,11 +112,11 @@ final class WebViewExternalMessageHandlerTests: XCTestCase {
]

sut.handleExternalMessage(dictionary2)
XCTAssertTrue(mockWebViewController.presentOverlayControllerCalled)
XCTAssertTrue(mockWebViewController.presentAlertControllerCalled)
let swiftMessage = SwiftMessages.current(id: "BarcodeScannerMessage")
XCTAssertNotNil(swiftMessage)
}

func testHandleExternalMessageStoreInPlatformKeychainOpenTransferFlow() {
@MainActor func testHandleExternalMessageStoreInPlatformKeychainOpenTransferFlow() {
let dictionary: [String: Any] = [
"id": 1,
"message": "",
Expand All @@ -140,7 +141,7 @@ final class WebViewExternalMessageHandlerTests: XCTestCase {
XCTAssertEqual(mockWebViewController.overlayedController?.view.backgroundColor, .clear)
}

func testHandleExternalMessageImportThreadCredentialsStartImportFlow() {
@MainActor func testHandleExternalMessageImportThreadCredentialsStartImportFlow() {
let dictionary: [String: Any] = [
"id": 1,
"message": "",
Expand All @@ -161,7 +162,7 @@ final class WebViewExternalMessageHandlerTests: XCTestCase {
XCTAssertEqual(mockWebViewController.overlayedController?.view.backgroundColor, .clear)
}

func testHandleExternalMessageShowAssistShowsAssist() {
@MainActor func testHandleExternalMessageShowAssistShowsAssist() {
let dictionary: [String: Any] = [
"id": 1,
"message": "",
Expand Down
Loading