Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add QR code scanner #2576

Merged
merged 12 commits into from
Feb 19, 2024
36 changes: 36 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,11 @@
420FE84B2B556BB100878E06 /* CarPlayActionsTemplate+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420FE84A2B556BB100878E06 /* CarPlayActionsTemplate+Build.swift */; };
420FE84E2B556CE500878E06 /* CarPlayEntitiesListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420FE84D2B556CE500878E06 /* CarPlayEntitiesListViewModel.swift */; };
420FE8502B556F7500878E06 /* CarPlayEntitiesListTemplate+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420FE84F2B556F7500878E06 /* CarPlayEntitiesListTemplate+Build.swift */; };
42266B112B740E4C00E94A71 /* BarcodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B102B740E4C00E94A71 /* BarcodeScannerView.swift */; };
42266B1D2B741FB400E94A71 /* BarcodeScannerCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B172B741FB300E94A71 /* BarcodeScannerCameraView.swift */; };
42266B1F2B741FB400E94A71 /* BarcodeScannerDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B192B741FB300E94A71 /* BarcodeScannerDataModel.swift */; };
42266B202B741FB400E94A71 /* BarcodeScannerCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B1A2B741FB400E94A71 /* BarcodeScannerCamera.swift */; };
42266B252B7A4BA900E94A71 /* BarcodeScannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */; };
424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F452B188946008C8DF3 /* WidgetBackground.swift */; };
424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */; };
424DD05A2B3509170057E456 /* CarPlayActionsTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424DD0592B3509170057E456 /* CarPlayActionsTemplate.swift */; };
Expand Down Expand Up @@ -1569,6 +1574,11 @@
420FE84A2B556BB100878E06 /* CarPlayActionsTemplate+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarPlayActionsTemplate+Build.swift"; sourceTree = "<group>"; };
420FE84D2B556CE500878E06 /* CarPlayEntitiesListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayEntitiesListViewModel.swift; sourceTree = "<group>"; };
420FE84F2B556F7500878E06 /* CarPlayEntitiesListTemplate+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarPlayEntitiesListTemplate+Build.swift"; sourceTree = "<group>"; };
42266B102B740E4C00E94A71 /* BarcodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeScannerView.swift; sourceTree = "<group>"; };
42266B172B741FB300E94A71 /* BarcodeScannerCameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarcodeScannerCameraView.swift; sourceTree = "<group>"; };
42266B192B741FB300E94A71 /* BarcodeScannerDataModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarcodeScannerDataModel.swift; sourceTree = "<group>"; };
42266B1A2B741FB400E94A71 /* BarcodeScannerCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarcodeScannerCamera.swift; sourceTree = "<group>"; };
42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeScannerViewModel.swift; sourceTree = "<group>"; };
4242A2B12B2B5C8000E9F001 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/AppIntentVocabulary.plist; sourceTree = "<group>"; };
4242A2B22B2B5C8100E9F001 /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "ca-ES"; path = "ca-ES.lproj/AppIntentVocabulary.plist"; sourceTree = "<group>"; };
4242A2B32B2B5C8100E9F001 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hans"; path = "zh-Hans.lproj/AppIntentVocabulary.plist"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3052,6 +3062,26 @@
path = Entities;
sourceTree = "<group>";
};
42266B0F2B740E3600E94A71 /* QRCodeScanner */ = {
isa = PBXGroup;
children = (
42266B152B741F9F00E94A71 /* Camera */,
42266B102B740E4C00E94A71 /* BarcodeScannerView.swift */,
42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */,
);
path = QRCodeScanner;
sourceTree = "<group>";
};
42266B152B741F9F00E94A71 /* Camera */ = {
isa = PBXGroup;
children = (
42266B1A2B741FB400E94A71 /* BarcodeScannerCamera.swift */,
42266B172B741FB300E94A71 /* BarcodeScannerCameraView.swift */,
42266B192B741FB300E94A71 /* BarcodeScannerDataModel.swift */,
);
path = Camera;
sourceTree = "<group>";
};
425573C52B55729E00145217 /* Servers */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3493,6 +3523,7 @@
B657A8E81CA646EB00121384 /* App */ = {
isa = PBXGroup;
children = (
42266B0F2B740E3600E94A71 /* QRCodeScanner */,
B657A8E91CA646EB00121384 /* AppDelegate.swift */,
D03D893720E0AF1B00D4F28D /* ClientEvents */,
11A183B22511BCF300CA326A /* LifecycleManager.swift */,
Expand Down Expand Up @@ -5610,6 +5641,7 @@
425573CC2B5574AD00145217 /* CarPlayAreasZonesTemplate+Build.swift in Sources */,
B626AAF11D8F972800A0D225 /* SettingsDetailViewController.swift in Sources */,
1127381C2622B6F300F5E312 /* DebugSettingsViewController.swift in Sources */,
42266B252B7A4BA900E94A71 /* BarcodeScannerViewModel.swift in Sources */,
11DE823024FAE66F00E636B8 /* UIWindow+Additions.swift in Sources */,
11DA6B4F2713912F008ADFAF /* OnboardingPermissionViewController.swift in Sources */,
42CA28BB2B1028330093B31A /* SimulatorThreadClientService.swift in Sources */,
Expand All @@ -5633,6 +5665,7 @@
B641BC251E20A17B002CCBC1 /* OpenInChromeController.swift in Sources */,
B661FB6A226BBDA900E541DD /* SettingsViewController.swift in Sources */,
119D765F2492F8FA00183C5F /* UIApplication+BackgroundTask.swift in Sources */,
42266B202B741FB400E94A71 /* BarcodeScannerCamera.swift in Sources */,
11195F6F267EFC8E003DF674 /* NotificationManagerLocalPushInterfaceDirect.swift in Sources */,
FD3BC66329B9FF8F00B19FBE /* CarPlaySceneDelegate.swift in Sources */,
11C4629424B189B100031902 /* NotificationRateLimitsAPI.swift in Sources */,
Expand Down Expand Up @@ -5672,6 +5705,7 @@
42F1DA612B4D4F31002729BC /* CarPlayNoServerAlert.swift in Sources */,
11C590ED24A832CA0066085D /* YamlSection.swift in Sources */,
42F1DA5B2B4BF7DF002729BC /* WindowSizeObserver.swift in Sources */,
42266B1D2B741FB400E94A71 /* BarcodeScannerCameraView.swift in Sources */,
11EFCDD624F5FA8D00314D85 /* WebViewSceneDelegate.swift in Sources */,
1185DF94271FBA6100ED7D9A /* OnboardingAuthDetails.swift in Sources */,
420FE84B2B556BB100878E06 /* CarPlayActionsTemplate+Build.swift in Sources */,
Expand Down Expand Up @@ -5712,6 +5746,7 @@
1185DFB3271FF53800ED7D9A /* OnboardingAuthStepSensors.swift in Sources */,
11108D632634C8FE009DAB0F /* LearnMoreButtonRow.swift in Sources */,
425573D12B5576E600145217 /* CarPlayDomainsListTemplate+Build.swift in Sources */,
42266B112B740E4C00E94A71 /* BarcodeScannerView.swift in Sources */,
11B62DBE24F2EDD800E5CB55 /* EurekaCondition+Additions.swift in Sources */,
B661FB74226C110A00E541DD /* OnboardingWelcomeViewController.swift in Sources */,
1164DA2125FBEE8600515E8A /* TemplateEditViewController.swift in Sources */,
Expand All @@ -5735,6 +5770,7 @@
42DD84162B14D7AC00936F16 /* WebViewExternalBusMessage.swift in Sources */,
11EFCDD324F5F39100314D85 /* WebViewWindowController.swift in Sources */,
11EFCDE024F60E5900314D85 /* BasicSceneDelegate.swift in Sources */,
42266B1F2B741FB400E94A71 /* BarcodeScannerDataModel.swift in Sources */,
425573C92B5572DB00145217 /* CarPlayServerListViewModel.swift in Sources */,
11A71C6F24A4644A00D9565F /* ZoneManagerIgnoreReason.swift in Sources */,
1101568324D770B2009424C9 /* iOSTagManager.swift in Sources */,
Expand Down
136 changes: 136 additions & 0 deletions Sources/App/QRCodeScanner/BarcodeScannerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import Shared
import SwiftUI

struct BarcodeScannerView: View {
@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
private let flashlightIcon = MaterialDesignIcons.flashlightIcon.image(
ofSize: .init(width: 24, height: 24),
color: .white
)

Check warning on line 13 in Sources/App/QRCodeScanner/BarcodeScannerView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerView.swift#L8-L13

Added lines #L8 - L13 were not covered by tests

private let title: String
private let description: String
private let alternativeOptionLabel: String?

init(
title: String,
description: String,
alternativeOptionLabel: String? = nil,
incomingMessageId: Int
) {
self.title = title
self.description = description
self.alternativeOptionLabel = alternativeOptionLabel
self._viewModel = .init(wrappedValue: .init(incomingMessageId: incomingMessageId))
}

Check warning on line 29 in Sources/App/QRCodeScanner/BarcodeScannerView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerView.swift#L24-L29

Added lines #L24 - L29 were not covered by tests

var body: some View {
ZStack(alignment: .top) {
ZStack {
cameraBackground
cameraSquare
}
.ignoresSafeArea()
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)

topInformation
}
.onAppear {
cameraDataModel.camera.qrFound = { code, format in
viewModel.scannedCode(code, format: format)
}
}
}

Check warning on line 48 in Sources/App/QRCodeScanner/BarcodeScannerView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerView.swift#L31-L48

Added lines #L31 - L48 were not covered by tests

private var topInformation: some View {
VStack(spacing: 8) {
Button(action: {
bgoncal marked this conversation as resolved.
Show resolved Hide resolved
viewModel.aborted(.canceled)
dismiss()
}, label: {
Image(systemName: "xmark")
.font(.title2)
.foregroundColor(.white.opacity(0.8))
.frame(maxWidth: .infinity, alignment: .leading)
})
.accessibilityHint(.init(L10n.closeLabel))
Group {
Text(title)
.padding(.top)
.font(.title2)
Text(description)
.font(.subheadline)
}
.foregroundColor(.white)

if let alternativeOptionLabel {
Button {
viewModel.aborted(.alternativeOptions)
dismiss()
} label: {
Text(alternativeOptionLabel)
.font(.subheadline)
.foregroundColor(.accentColor)
}
.padding(.top)
}
}
.padding()
}

Check warning on line 84 in Sources/App/QRCodeScanner/BarcodeScannerView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerView.swift#L50-L84

Added lines #L50 - L84 were not covered by tests

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

Check warning on line 94 in Sources/App/QRCodeScanner/BarcodeScannerView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerView.swift#L86-L94

Added lines #L86 - L94 were not covered by tests

private var cameraSquare: some View {
BarcodeScannerCameraView(model: cameraDataModel, shouldStartCamera: false)
.ignoresSafeArea()
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
.mask {
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
.frame(width: cameraSquareSize, height: cameraSquareSize)
}
.overlay {
ZStack(alignment: .bottomTrailing) {
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
.stroke(Color.blue, lineWidth: 1)
.frame(width: cameraSquareSize, height: cameraSquareSize)
Button(action: {
toggleFlashlight()
}, label: {
Image(uiImage: flashlightIcon)
.padding()
.background(Color(uiColor: .init(hex: "#384956")))
.mask(Circle())
.offset(x: -22, y: -22)
})
}
}
}

Check warning on line 121 in Sources/App/QRCodeScanner/BarcodeScannerView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerView.swift#L96-L121

Added lines #L96 - L121 were not covered by tests

private func toggleFlashlight() {
cameraDataModel.toggleFlashlight()
}

Check warning on line 125 in Sources/App/QRCodeScanner/BarcodeScannerView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerView.swift#L123-L125

Added lines #L123 - L125 were not covered by tests
}

#Preview {
BarcodeScannerView(title: "Scan QR-code", description: "Find the code on your device", incomingMessageId: 1)
}

final class BarcodeScannerHostingController: UIHostingController<BarcodeScannerView> {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
[.portrait]
}

Check warning on line 135 in Sources/App/QRCodeScanner/BarcodeScannerView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerView.swift#L133-L135

Added lines #L133 - L135 were not covered by tests
}
45 changes: 45 additions & 0 deletions Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
import Shared

final class BarcodeScannerViewModel: ObservableObject {
enum AbortReason: String {
case canceled
case alternativeOptions = "alternative_options"
}

private let incomingMessageId: Int

init(incomingMessageId: Int) {
self.incomingMessageId = incomingMessageId
}

Check warning on line 14 in Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift#L12-L14

Added lines #L12 - L14 were not covered by tests

func scannedCode(_ code: String, format: String) {
Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise)
.done { [weak self] controller in
guard let incomingMessageId = self?.incomingMessageId else { return }
controller
.sendExternalBus(message: .init(
id: incomingMessageId,
command: WebViewExternalBusOutgoingMessage.barCodeScanResult.rawValue,
payload: [
"rawValue": code,
"format": format,
]
))
}
}

Check warning on line 30 in Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift#L16-L30

Added lines #L16 - L30 were not covered by tests

func aborted(_ reason: AbortReason) {
Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise)
.done { [weak self] controller in
guard let incomingMessageId = self?.incomingMessageId else { return }
controller.sendExternalBus(message: .init(
id: incomingMessageId,
command: WebViewExternalBusOutgoingMessage.barCodeScanAborted.rawValue,
payload: [
"reason": reason.rawValue,
]
))
}
}

Check warning on line 44 in Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift#L32-L44

Added lines #L32 - L44 were not covered by tests
}
Loading
Loading