mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-02-14 17:49:54 +00:00
Use more SwiftUI elements and clean up interface
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
@@ -13,19 +14,33 @@ class AccessoryController: ObservableObject {
|
||||
static let shared = AccessoryController()
|
||||
|
||||
@Published var accessories: [Accessory]
|
||||
var cancellables = [AnyCancellable]()
|
||||
var saveCancellable: AnyCancellable?
|
||||
|
||||
var accessoryObserver: AnyCancellable?
|
||||
|
||||
init() {
|
||||
self.accessories = KeychainController.loadAccessoriesFromKeychain()
|
||||
self.accessoryObserver = self.accessories.publisher
|
||||
.sink { _ in
|
||||
try? self.save()
|
||||
}
|
||||
initObserver()
|
||||
}
|
||||
|
||||
func initObserver() {
|
||||
self.accessories.forEach({
|
||||
let c = $0.objectWillChange.sink(receiveValue: { self.objectWillChange.send() })
|
||||
|
||||
// Important: You have to keep the returned value allocated,
|
||||
// otherwise the sink subscription gets cancelled
|
||||
self.cancellables.append(c)
|
||||
})
|
||||
self.saveCancellable = self.$accessories.sink { _ in
|
||||
// FIXME: accessories actually don't change
|
||||
try? self.save()
|
||||
}
|
||||
}
|
||||
|
||||
init(accessories: [Accessory]) {
|
||||
self.accessories = accessories
|
||||
initObserver()
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
@@ -49,8 +64,6 @@ class AccessoryController: ObservableObject {
|
||||
|
||||
accessory.lastLocation = report?.location
|
||||
accessory.locationTimestamp = report?.timestamp
|
||||
|
||||
self.accessories[idx] = accessory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,16 @@ import Foundation
|
||||
import Security
|
||||
import SwiftUI
|
||||
|
||||
class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
let name: String
|
||||
class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
@Published var name: String
|
||||
let id: Int
|
||||
let privateKey: Data
|
||||
let color: Color
|
||||
let icon: String
|
||||
|
||||
@Published var color: Color
|
||||
@Published var icon: String {
|
||||
didSet {
|
||||
print("Setting icon")
|
||||
}
|
||||
}
|
||||
@Published var lastLocation: CLLocation?
|
||||
@Published var locationTimestamp: Date?
|
||||
|
||||
@@ -92,6 +95,10 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
try self.hashedPublicKey().base64EncodedString()
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(self.id)
|
||||
}
|
||||
|
||||
private func hashedPublicKey() throws -> Data {
|
||||
let publicKey = try self.getAdvertisementKey()
|
||||
var sha = SHA256()
|
||||
|
||||
@@ -10,12 +10,17 @@ import SwiftUI
|
||||
|
||||
struct AccessoryListEntry: View {
|
||||
var accessory: Accessory
|
||||
@Binding var accessoryIcon: String
|
||||
@Binding var accessoryColor: Color
|
||||
@Binding var accessoryName: String
|
||||
@Binding var alertType: OpenHaystackMainView.AlertType?
|
||||
var delete: (Accessory) -> Void
|
||||
var deployAccessoryToMicrobit: (Accessory) -> Void
|
||||
var zoomOn: (Accessory) -> Void
|
||||
let formatter = DateFormatter()
|
||||
|
||||
@State var editingName: Bool = false
|
||||
|
||||
func timestampView() -> some View {
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .short
|
||||
@@ -30,33 +35,22 @@ struct AccessoryListEntry: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
HStack {
|
||||
Circle()
|
||||
.strokeBorder(accessory.color, lineWidth: 2.0)
|
||||
.background(
|
||||
ZStack {
|
||||
Circle().fill(Color("PinColor"))
|
||||
Image(systemName: accessory.icon)
|
||||
.padding(3)
|
||||
}
|
||||
)
|
||||
.frame(width: 40, height: 40)
|
||||
IconSelectionView(selectedImageName: $accessoryIcon, selectedColor: $accessoryColor)
|
||||
|
||||
Button(
|
||||
action: {
|
||||
self.zoomOn(self.accessory)
|
||||
},
|
||||
label: {
|
||||
VStack(alignment: .leading) {
|
||||
Text(accessory.name)
|
||||
.font(.headline)
|
||||
self.timestampView()
|
||||
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
VStack(alignment: .leading) {
|
||||
if self.editingName {
|
||||
TextField("Enter accessory name", text: $accessoryName, onCommit: { self.editingName = false })
|
||||
.font(.headline)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
} else {
|
||||
Text(accessory.name)
|
||||
.font(.headline)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
self.timestampView()
|
||||
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -69,9 +63,9 @@ struct AccessoryListEntry: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
|
||||
.padding(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
|
||||
.contextMenu {
|
||||
Button("Rename", action: { self.editingName = true })
|
||||
Button("Delete", action: { self.delete(accessory) })
|
||||
Divider()
|
||||
Button("Copy advertisment key (Base64)", action: { self.copyPublicKey(of: accessory) })
|
||||
|
||||
@@ -71,40 +71,6 @@ class AccessoryAnnotationView: MKAnnotationView {
|
||||
self.canShowCallout = true
|
||||
}
|
||||
|
||||
// override func draw(_ dirtyRect: NSRect) {
|
||||
// guard let accessoryAnnotation = self.annotation as? AccessoryAnnotation else {
|
||||
// super.draw(dirtyRect)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let path = NSBezierPath(ovalIn: dirtyRect)
|
||||
// path.lineWidth = 2.0
|
||||
//
|
||||
// guard let cgColor = accessoryAnnotation.accessory.color.cgColor,
|
||||
// let strokeColor = NSColor(cgColor: cgColor)?.withAlphaComponent(0.8) else {return}
|
||||
//
|
||||
// NSColor(named: NSColor.Name("PinColor"))?.setFill()
|
||||
//
|
||||
// path.fill()
|
||||
//
|
||||
// strokeColor.setStroke()
|
||||
// path.stroke()
|
||||
//
|
||||
// let accessory = accessoryAnnotation.accessory
|
||||
//
|
||||
// guard let image = NSImage(systemSymbolName: accessory.icon, accessibilityDescription: accessory.name) else {return}
|
||||
//
|
||||
// let ratio = image.size.width / image.size.height
|
||||
// let imageWidth: CGFloat = 20
|
||||
// let imageHeight = imageWidth / ratio
|
||||
// let imageRect = NSRect(
|
||||
// x: dirtyRect.width/2 - imageWidth/2,
|
||||
// y: dirtyRect.height/2 - imageHeight/2,
|
||||
// width: imageWidth, height: imageHeight)
|
||||
//
|
||||
// image.draw(in: imageRect)
|
||||
// }
|
||||
|
||||
struct AccessoryPinView: View {
|
||||
var accessory: Accessory
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import SwiftUI
|
||||
struct IconSelectionView: View {
|
||||
|
||||
@State var showImagePicker = false
|
||||
@State var color: Color = .red
|
||||
@Binding var selectedImageName: String
|
||||
@Binding var selectedColor: Color
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -24,11 +24,15 @@ struct IconSelectionView: View {
|
||||
},
|
||||
label: {
|
||||
Circle()
|
||||
.strokeBorder(Color.gray, lineWidth: 0.5)
|
||||
.strokeBorder(self.selectedColor, lineWidth: 2)
|
||||
|
||||
.background(
|
||||
Image(systemName: self.selectedImageName)
|
||||
ZStack {
|
||||
Circle().fill(Color("PinColor"))
|
||||
Image(systemName: self.selectedImageName)
|
||||
}
|
||||
)
|
||||
.frame(width: 30, height: 30)
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
@@ -45,11 +49,12 @@ struct IconSelectionView: View {
|
||||
|
||||
struct ColorSelectionView_Previews: PreviewProvider {
|
||||
@State static var selectedImageName: String = "briefcase.fill"
|
||||
@State static var selectedColor: Color = .red
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
IconSelectionView(selectedImageName: self.$selectedImageName)
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName, dismiss: {})
|
||||
IconSelectionView(selectedImageName: self.$selectedImageName, selectedColor: self.$selectedColor)
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName, dismiss: { () })
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,24 +68,26 @@ struct ImageSelectionList: View {
|
||||
let dismiss: () -> Void
|
||||
|
||||
var body: some View {
|
||||
List(self.selectableIcons, id: \.self) { iconName in
|
||||
Button(
|
||||
action: {
|
||||
self.selectedImageName = iconName
|
||||
self.dismiss()
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: iconName)
|
||||
Spacer()
|
||||
VStack {
|
||||
List(self.selectableIcons, id: \.self) { iconName in
|
||||
Button(
|
||||
action: {
|
||||
self.selectedImageName = iconName
|
||||
self.dismiss()
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: iconName)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.contentShape(Rectangle())
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.frame(width: 100)
|
||||
}
|
||||
.frame(width: 100)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,47 +28,18 @@ struct ManageAccessoriesView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Create a new tracking accessory")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
|
||||
Text("A BBC Microbit can be used to track anything you care about. Connect it over USB, name the accessory (e.g. Backpack) generate the key and deploy it")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
HStack {
|
||||
TextField("Name", text: self.$keyName)
|
||||
ColorPicker("", selection: self.$accessoryColor)
|
||||
.frame(maxWidth: 50, maxHeight: 20)
|
||||
IconSelectionView(selectedImageName: self.$selectedIcon)
|
||||
}
|
||||
|
||||
Button(
|
||||
action: self.addAccessory,
|
||||
label: {
|
||||
Text("Generate key and deploy")
|
||||
}
|
||||
)
|
||||
.disabled(self.keyName.isEmpty)
|
||||
.padding(.bottom)
|
||||
|
||||
Divider()
|
||||
|
||||
Text("Your accessories")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
|
||||
if self.accessories.isEmpty {
|
||||
Spacer()
|
||||
Text("No accessories have been added yet. Go ahead and add one above")
|
||||
Text("No accessories have been added yet. Go ahead and add one via the '+' icon.")
|
||||
.multilineTextAlignment(.center)
|
||||
Spacer()
|
||||
} else {
|
||||
self.accessoryList
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.sheet(isPresented: self.$showESP32DeploySheet, content: {
|
||||
ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType)
|
||||
@@ -77,16 +48,27 @@ struct ManageAccessoriesView: View {
|
||||
|
||||
/// Accessory List view.
|
||||
var accessoryList: some View {
|
||||
List(self.accessories) { accessory in
|
||||
List(self.accessories, id: \.self, selection: $focusedAccessory) { accessory in
|
||||
AccessoryListEntry(
|
||||
accessory: accessory,
|
||||
accessoryIcon: Binding(
|
||||
get: { accessory.icon },
|
||||
set: { accessory.icon = $0 }
|
||||
),
|
||||
accessoryColor: Binding(
|
||||
get: { accessory.color },
|
||||
set: { accessory.color = $0 }
|
||||
),
|
||||
accessoryName: Binding(
|
||||
get: { accessory.name },
|
||||
set: { accessory.name = $0 }
|
||||
),
|
||||
alertType: self.$alertType,
|
||||
delete: self.delete(accessory:),
|
||||
deployAccessoryToMicrobit: self.deploy(accessory:),
|
||||
deployAccessoryToMicrobit: self.deployAccessoryToMicrobit(accessory:),
|
||||
zoomOn: { self.focusedAccessory = $0 })
|
||||
}
|
||||
.background(Color.clear)
|
||||
.cornerRadius(15.0)
|
||||
.listStyle(SidebarListStyle())
|
||||
}
|
||||
|
||||
/// Delete an accessory from the list of accessories.
|
||||
|
||||
@@ -25,7 +25,7 @@ struct OpenHaystackMainView: View {
|
||||
@State var searchPartyTokenLoaded = false
|
||||
@State var mapType: MKMapType = .standard
|
||||
@State var isLoading = false
|
||||
@State var focusedAccessory: Accessory?
|
||||
@State var focusedAccessory: Accessory? = nil
|
||||
@State var accessoryToDeploy: Accessory?
|
||||
|
||||
@State var showESP32DeploySheet = false
|
||||
@@ -33,25 +33,32 @@ struct OpenHaystackMainView: View {
|
||||
var body: some View {
|
||||
|
||||
NavigationView {
|
||||
|
||||
ManageAccessoriesView(
|
||||
alertType: self.$alertType,
|
||||
focusedAccessory: self.$focusedAccessory,
|
||||
accessoryToDeploy: self.$accessoryToDeploy,
|
||||
showESP32DeploySheet: self.$showESP32DeploySheet)
|
||||
.toolbar(content: {
|
||||
Spacer()
|
||||
Button(action: self.addAccessory) {
|
||||
Label("Add accessory", systemImage: "plus")
|
||||
}
|
||||
})
|
||||
.navigationTitle(self.focusedAccessory?.name ?? "OpenHaystack")
|
||||
|
||||
VStack {
|
||||
ZStack {
|
||||
self.mapView
|
||||
if self.popUpAlertType != nil {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
PopUpAlertView(alertType: self.popUpAlertType!)
|
||||
.transition(AnyTransition.move(edge: .bottom))
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all)
|
||||
.alert(
|
||||
item: self.$alertType,
|
||||
content: { alertType in
|
||||
@@ -75,7 +82,6 @@ struct OpenHaystackMainView: View {
|
||||
self.onAppear()
|
||||
}
|
||||
}
|
||||
.navigationTitle("OpenHaystack")
|
||||
}
|
||||
|
||||
// MARK: Subviews
|
||||
|
||||
Reference in New Issue
Block a user