mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-08-20 03:56:14 +00:00
Add swift-format as an Xcode build phase
This commit is contained in:
@@ -31,12 +31,12 @@ class AccessoryController: ObservableObject {
|
||||
func updateWithDecryptedReports(devices: [FindMyDevice]) {
|
||||
// Assign last locations
|
||||
for device in FindMyController.shared.devices {
|
||||
if let idx = self.accessories.firstIndex(where: {$0.id == Int(device.deviceId)}) {
|
||||
if let idx = self.accessories.firstIndex(where: { $0.id == Int(device.deviceId) }) {
|
||||
self.objectWillChange.send()
|
||||
let accessory = self.accessories[idx]
|
||||
|
||||
let report = device.decryptedReports?
|
||||
.sorted(by: {$0.timestamp ?? Date.distantPast > $1.timestamp ?? Date.distantPast })
|
||||
.sorted(by: { $0.timestamp ?? Date.distantPast > $1.timestamp ?? Date.distantPast })
|
||||
.first
|
||||
|
||||
accessory.lastLocation = report?.location
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
import OSLog
|
||||
import Security
|
||||
|
||||
struct KeychainController {
|
||||
|
||||
static func loadAccessoriesFromKeychain(test: Bool=false) -> [Accessory] {
|
||||
static func loadAccessoriesFromKeychain(test: Bool = false) -> [Accessory] {
|
||||
var query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecMatchLimit: kSecMatchLimitOne,
|
||||
kSecReturnData: true
|
||||
kSecReturnData: true,
|
||||
]
|
||||
|
||||
if test {
|
||||
@@ -27,7 +27,8 @@ struct KeychainController {
|
||||
var result: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
guard status == errSecSuccess,
|
||||
let resultData = result as? Data else {
|
||||
let resultData = result as? Data
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -42,13 +43,13 @@ struct KeychainController {
|
||||
return []
|
||||
}
|
||||
|
||||
static func storeInKeychain(accessories: [Accessory], test: Bool=false) throws {
|
||||
static func storeInKeychain(accessories: [Accessory], test: Bool = false) throws {
|
||||
// Store or update
|
||||
var attributes: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecValueData: try PropertyListEncoder().encode(accessories)
|
||||
kSecValueData: try PropertyListEncoder().encode(accessories),
|
||||
]
|
||||
|
||||
if test {
|
||||
@@ -62,7 +63,7 @@ struct KeychainController {
|
||||
var query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY"
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
]
|
||||
|
||||
if test {
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
import OSLog
|
||||
import AppKit
|
||||
|
||||
let mailBundleName = "OpenHaystackMail"
|
||||
|
||||
/// Manages plugin installation
|
||||
/// Manages plugin installation.
|
||||
struct MailPluginManager {
|
||||
|
||||
let pluginsFolderURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Mail/Bundles")
|
||||
@@ -22,7 +22,7 @@ struct MailPluginManager {
|
||||
return FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
}
|
||||
|
||||
/// Shows a NSSavePanel to install the mail plugin at the required place
|
||||
/// Shows a NSSavePanel to install the mail plugin at the required place.
|
||||
func askForPermission() -> Bool {
|
||||
|
||||
let panel = NSSavePanel()
|
||||
@@ -73,11 +73,12 @@ struct MailPluginManager {
|
||||
|
||||
}
|
||||
|
||||
/// Copy a folder recursively
|
||||
/// Copy a folder recursively.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: Folder source
|
||||
/// - to: Folder destination
|
||||
/// - Throws: An error if copying or acessing files fails
|
||||
/// - Throws: An error if copying or acessing files fails
|
||||
func copyFolder(from: URL, to: URL) throws {
|
||||
// Create the folder
|
||||
try? FileManager.default.createDirectory(at: to, withIntermediateDirectories: false, attributes: nil)
|
||||
@@ -102,11 +103,13 @@ struct MailPluginManager {
|
||||
try FileManager.default.removeItem(at: pluginURL)
|
||||
}
|
||||
|
||||
/// Copy plugin to downloads folder
|
||||
/// Copy plugin to downloads folder.
|
||||
///
|
||||
/// - Throws: An error if the copy fails, because of missing permissions
|
||||
func pluginDownload() throws {
|
||||
func pluginDownload() throws {
|
||||
guard let localPluginURL = Bundle.main.url(forResource: mailBundleName, withExtension: "mailbundle"),
|
||||
let downloadsFolder = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
|
||||
let downloadsFolder = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
||||
else {
|
||||
throw PluginError.downloadFailed
|
||||
}
|
||||
|
||||
|
||||
@@ -9,19 +9,21 @@ import Foundation
|
||||
|
||||
struct MicrobitController {
|
||||
|
||||
/// Find all microbits connected to this mac
|
||||
/// Find all microbits connected to this Mac.
|
||||
///
|
||||
/// - Throws: If a volume is inaccessible
|
||||
/// - Returns: an array of urls
|
||||
static func findMicrobits() throws -> [URL] {
|
||||
let fm = FileManager.default
|
||||
let volumes = try fm.contentsOfDirectory(atPath: "/Volumes")
|
||||
|
||||
let microbits: [URL] = volumes.filter({$0.lowercased().contains("microbit")}).map({URL(fileURLWithPath: "/Volumes").appendingPathComponent($0)})
|
||||
let microbits: [URL] = volumes.filter({ $0.lowercased().contains("microbit") }).map({ URL(fileURLWithPath: "/Volumes").appendingPathComponent($0) })
|
||||
|
||||
return microbits
|
||||
}
|
||||
|
||||
/// Deploy the firmware to a USB connected microbit at the given URL
|
||||
/// Deploy the firmware to a USB connected microbit at the given URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - microbitURL: URL to the microbit
|
||||
/// - firmwareFile: Firmware file as binary data
|
||||
@@ -32,6 +34,7 @@ struct MicrobitController {
|
||||
}
|
||||
|
||||
/// Patch the given firmware.
|
||||
///
|
||||
/// This will replace the pattern data (the place for the key) with the actual key
|
||||
/// - Parameters:
|
||||
/// - firmware: The firmware data that should be patched
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import Security
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
let name: String
|
||||
@@ -39,9 +39,10 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
self.privateKey = try container.decode(Data.self, forKey: .privateKey)
|
||||
self.icon = (try? container.decode(String.self, forKey: .icon)) ?? "briefcase.fill"
|
||||
|
||||
if var colorComponents = try? container.decode([CGFloat].self, forKey: .colorComponents),
|
||||
if var colorComponents = try? container.decode([CGFloat].self, forKey: .colorComponents),
|
||||
let spaceName = try? container.decode(String.self, forKey: .colorSpaceName),
|
||||
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents) {
|
||||
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents)
|
||||
{
|
||||
self.color = Color(cgColor)
|
||||
} else {
|
||||
self.color = Color.white
|
||||
@@ -57,7 +58,8 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
try container.encode(self.icon, forKey: .icon)
|
||||
|
||||
if let colorComponents = self.color.cgColor?.components,
|
||||
let colorSpace = self.color.cgColor?.colorSpace?.name {
|
||||
let colorSpace = self.color.cgColor?.colorSpace?.name
|
||||
{
|
||||
try container.encode(colorComponents, forKey: .colorComponents)
|
||||
try container.encode(colorSpace as String, forKey: .colorSpaceName)
|
||||
}
|
||||
@@ -79,7 +81,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
// Drop the first byte to just have the 28 bytes version
|
||||
publicKey = publicKey.dropFirst()
|
||||
assert(publicKey.count == 28)
|
||||
guard publicKey.count == 28 else {throw KeyError.keyDerivationFailed}
|
||||
guard publicKey.count == 28 else { throw KeyError.keyDerivationFailed }
|
||||
|
||||
return publicKey
|
||||
}
|
||||
@@ -103,20 +105,22 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
|
||||
func toFindMyDevice() throws -> FindMyDevice {
|
||||
|
||||
let findMyKey = FindMyKey(advertisedKey: try self.getAdvertisementKey(),
|
||||
hashedKey: try self.hashedPublicKey(),
|
||||
privateKey: self.privateKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: nil,
|
||||
fullKey: nil)
|
||||
let findMyKey = FindMyKey(
|
||||
advertisedKey: try self.getAdvertisementKey(),
|
||||
hashedKey: try self.hashedPublicKey(),
|
||||
privateKey: self.privateKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: nil,
|
||||
fullKey: nil)
|
||||
|
||||
return FindMyDevice(deviceId: String(self.id),
|
||||
keys: [findMyKey],
|
||||
catalinaBigSurKeyFiles: nil,
|
||||
reports: nil,
|
||||
decryptedReports: nil)
|
||||
return FindMyDevice(
|
||||
deviceId: String(self.id),
|
||||
keys: [findMyKey],
|
||||
catalinaBigSurKeyFiles: nil,
|
||||
reports: nil,
|
||||
decryptedReports: nil)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
|
||||
@@ -20,19 +20,19 @@ struct PreviewData {
|
||||
let longitude: Double = 13.413306
|
||||
|
||||
let backpack = try! Accessory(name: "Backpack", color: Color.green, iconName: "briefcase.fill")
|
||||
backpack.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
backpack.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let bag = try! Accessory(name: "Bag", color: Color.blue, iconName: "latch.2.case.fill")
|
||||
bag.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
bag.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let car = try! Accessory(name: "Car", color: Color.red, iconName: "car.fill")
|
||||
car.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
car.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let keys = try! Accessory(name: "Keys", color: Color.orange, iconName: "key.fill")
|
||||
keys.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
keys.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let items = try! Accessory(name: "Items", color: Color.gray, iconName: "mappin")
|
||||
items.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
items.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
return [backpack, bag, car, keys, items]
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct AccessoryListEntry: View {
|
||||
var accessory: Accessory
|
||||
@@ -18,39 +18,47 @@ struct AccessoryListEntry: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.zoomOn(self.accessory)
|
||||
}, label: {
|
||||
HStack {
|
||||
Text(accessory.name)
|
||||
Spacer()
|
||||
Button(
|
||||
action: {
|
||||
self.zoomOn(self.accessory)
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Text(accessory.name)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
})
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
HStack(alignment: .center) {
|
||||
|
||||
Button(action: {self.zoomOn(self.accessory)}, label: {
|
||||
Circle()
|
||||
.strokeBorder(accessory.color, lineWidth: 2.0)
|
||||
.background(
|
||||
ZStack {
|
||||
Circle().fill(Color("PinColor"))
|
||||
Image(systemName: accessory.icon)
|
||||
.padding(3)
|
||||
}
|
||||
Button(
|
||||
action: { self.zoomOn(self.accessory) },
|
||||
label: {
|
||||
Circle()
|
||||
.strokeBorder(accessory.color, lineWidth: 2.0)
|
||||
.background(
|
||||
ZStack {
|
||||
Circle().fill(Color("PinColor"))
|
||||
Image(systemName: accessory.icon)
|
||||
.padding(3)
|
||||
}
|
||||
)
|
||||
|
||||
.frame(width: 30, height: 30)
|
||||
})
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
Button(action: {
|
||||
self.deployAccessoryToMicrobit(accessory)
|
||||
}, label: {
|
||||
Text("Deploy")
|
||||
})
|
||||
Button(
|
||||
action: {
|
||||
self.deployAccessoryToMicrobit(accessory)
|
||||
},
|
||||
label: {
|
||||
Text("Deploy")
|
||||
})
|
||||
|
||||
}
|
||||
.padding(.trailing)
|
||||
@@ -60,10 +68,10 @@ struct AccessoryListEntry: View {
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.contextMenu {
|
||||
Button("Delete", action: {self.delete(accessory)})
|
||||
Button("Delete", action: { self.delete(accessory) })
|
||||
Divider()
|
||||
Button("Copy advertisment key (Base64)", action: {self.copyPublicKey(of: accessory)})
|
||||
Button("Copy key id (Base64)", action: {self.copyPublicKeyHash(of: accessory)})
|
||||
Button("Copy advertisment key (Base64)", action: { self.copyPublicKey(of: accessory) })
|
||||
Button("Copy key id (Base64)", action: { self.copyPublicKeyHash(of: accessory) })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class AccessoryAnnotationView: MKAnnotationView {
|
||||
}
|
||||
|
||||
func updateView() {
|
||||
guard let accessory = (self.annotation as? AccessoryAnnotation)?.accessory else {return}
|
||||
guard let accessory = (self.annotation as? AccessoryAnnotation)?.accessory else { return }
|
||||
self.pinView?.removeFromSuperview()
|
||||
self.pinView = NSHostingView(rootView: AccessoryPinView(accessory: accessory))
|
||||
|
||||
@@ -71,39 +71,39 @@ 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)
|
||||
// }
|
||||
// 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
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
struct AccessoryMapView: NSViewControllerRepresentable {
|
||||
@ObservedObject var accessoryController: AccessoryController
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
final class ActivityIndicator: NSViewRepresentable {
|
||||
|
||||
|
||||
@@ -16,24 +16,29 @@ struct IconSelectionView: View {
|
||||
var body: some View {
|
||||
|
||||
ZStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
self.showImagePicker.toggle()
|
||||
Button(
|
||||
action: {
|
||||
withAnimation {
|
||||
self.showImagePicker.toggle()
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Circle()
|
||||
.strokeBorder(Color.gray, lineWidth: 0.5)
|
||||
.background(
|
||||
Image(systemName: self.selectedImageName)
|
||||
)
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
}, label: {
|
||||
Circle()
|
||||
.strokeBorder(Color.gray, lineWidth: 0.5)
|
||||
.background(
|
||||
Image(systemName: self.selectedImageName)
|
||||
)
|
||||
.frame(width: 30, height: 30)
|
||||
})
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.popover(isPresented: self.$showImagePicker, content: {
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName) {
|
||||
self.showImagePicker = false
|
||||
}
|
||||
})
|
||||
.popover(
|
||||
isPresented: self.$showImagePicker,
|
||||
content: {
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName) {
|
||||
self.showImagePicker = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,16 +64,19 @@ struct ImageSelectionList: View {
|
||||
|
||||
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()
|
||||
Button(
|
||||
action: {
|
||||
self.selectedImageName = iconName
|
||||
self.dismiss()
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: iconName)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import MapKit
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct OpenHaystackMainView: View {
|
||||
|
||||
@@ -63,19 +63,25 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
}
|
||||
}
|
||||
.alert(item: self.$alertType, content: { alertType in
|
||||
return self.alert(for: alertType)
|
||||
})
|
||||
.alert(
|
||||
item: self.$alertType,
|
||||
content: { alertType in
|
||||
return self.alert(for: alertType)
|
||||
}
|
||||
)
|
||||
.onChange(of: self.searchPartyToken) { (searchPartyToken) in
|
||||
guard !searchPartyToken.isEmpty, self.accessories.isEmpty == false else {return}
|
||||
guard !searchPartyToken.isEmpty, self.accessories.isEmpty == false else { return }
|
||||
self.downloadLocationReports()
|
||||
}
|
||||
.onChange(of: self.popUpAlertType, perform: { popUpAlert in
|
||||
guard popUpAlert != nil else {return}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
self.popUpAlertType = nil
|
||||
.onChange(
|
||||
of: self.popUpAlertType,
|
||||
perform: { popUpAlert in
|
||||
guard popUpAlert != nil else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
self.popUpAlertType = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.onAppear {
|
||||
self.onAppear()
|
||||
}
|
||||
@@ -105,9 +111,12 @@ struct OpenHaystackMainView: View {
|
||||
IconSelectionView(selectedImageName: self.$selectedIcon)
|
||||
}
|
||||
|
||||
Button(action: self.addAccessory, label: {
|
||||
Text("Generate key and deploy")
|
||||
})
|
||||
Button(
|
||||
action: self.addAccessory,
|
||||
label: {
|
||||
Text("Generate key and deploy")
|
||||
}
|
||||
)
|
||||
.disabled(self.keyName.isEmpty)
|
||||
.padding(.bottom)
|
||||
|
||||
@@ -130,20 +139,21 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Accessory List view
|
||||
/// Accessory List view.
|
||||
var accessoryList: some View {
|
||||
List(self.accessories) { accessory in
|
||||
AccessoryListEntry(accessory: accessory,
|
||||
alertType: self.$alertType,
|
||||
delete: self.delete(accessory:),
|
||||
deployAccessoryToMicrobit: self.deployAccessoryToMicrobit(accessory:),
|
||||
zoomOn: {self.focusedAccessory = $0})
|
||||
AccessoryListEntry(
|
||||
accessory: accessory,
|
||||
alertType: self.$alertType,
|
||||
delete: self.delete(accessory:),
|
||||
deployAccessoryToMicrobit: self.deployAccessoryToMicrobit(accessory:),
|
||||
zoomOn: { self.focusedAccessory = $0 })
|
||||
}
|
||||
.background(Color.clear)
|
||||
.cornerRadius(15.0)
|
||||
}
|
||||
|
||||
/// Overlay for the map that is gray and shows an activity indicator when loading
|
||||
/// Overlay for the map that is gray and shows an activity indicator when loading.
|
||||
var mapOverlay: some View {
|
||||
ZStack {
|
||||
if self.isLoading {
|
||||
@@ -177,10 +187,13 @@ struct OpenHaystackMainView: View {
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.frame(width: 150, alignment: .center)
|
||||
|
||||
Button(action: self.downloadLocationReports, label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
Text("Reload")
|
||||
})
|
||||
Button(
|
||||
action: self.downloadLocationReports,
|
||||
label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
Text("Reload")
|
||||
}
|
||||
)
|
||||
.opacity(1.0)
|
||||
.disabled(self.accessories.isEmpty)
|
||||
}
|
||||
@@ -189,7 +202,7 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an accessory with the provided details
|
||||
/// Add an accessory with the provided details.
|
||||
func addAccessory() {
|
||||
let keyName = self.keyName
|
||||
self.keyName = ""
|
||||
@@ -223,7 +236,8 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
|
||||
guard !self.searchPartyToken.isEmpty,
|
||||
let tokenData = self.searchPartyToken.data(using: .utf8) else {
|
||||
let tokenData = self.searchPartyToken.data(using: .utf8)
|
||||
else {
|
||||
self.alertType = .searchPartyToken
|
||||
return
|
||||
}
|
||||
@@ -244,7 +258,7 @@ struct OpenHaystackMainView: View {
|
||||
FindMyController.shared.devices = findMyDevices
|
||||
FindMyController.shared.fetchReports(with: tokenData) { error in
|
||||
|
||||
let reports = FindMyController.shared.devices.compactMap({$0.reports}).flatMap({$0})
|
||||
let reports = FindMyController.shared.devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty {
|
||||
withAnimation {
|
||||
self.popUpAlertType = .noReportsFound
|
||||
@@ -257,7 +271,7 @@ struct OpenHaystackMainView: View {
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
guard error != nil else {return}
|
||||
guard error != nil else { return }
|
||||
os_log("Error: %@", String(describing: error))
|
||||
|
||||
}
|
||||
@@ -265,11 +279,11 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
}
|
||||
|
||||
/// Delete an accessory from the list of accessories
|
||||
/// Delete an accessory from the list of accessories.
|
||||
func delete(accessory: Accessory) {
|
||||
do {
|
||||
var accessories = self.accessories
|
||||
guard let idx = accessories.firstIndex(of: accessory) else {return}
|
||||
guard let idx = accessories.firstIndex(of: accessory) else { return }
|
||||
|
||||
accessories.remove(at: idx)
|
||||
|
||||
@@ -284,12 +298,13 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
}
|
||||
|
||||
/// Deploy the public key of the accessory to a BBC microbit
|
||||
/// Deploy the public key of the accessory to a BBC microbit.
|
||||
func deployAccessoryToMicrobit(accessory: Accessory) {
|
||||
do {
|
||||
let microbits = try MicrobitController.findMicrobits()
|
||||
guard let microBitURL = microbits.first,
|
||||
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin") else {
|
||||
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
|
||||
else {
|
||||
self.alertType = .deployFailed
|
||||
return
|
||||
}
|
||||
@@ -314,7 +329,8 @@ struct OpenHaystackMainView: View {
|
||||
/// Checks if the search party token can be fetched without the Mail Plugin. If true the plugin is not needed for this environment. (e.g. when SIP is disabled)
|
||||
let reportsFetcher = ReportsFetcher()
|
||||
if let token = reportsFetcher.fetchSearchpartyToken(),
|
||||
let tokenString = String(data: token, encoding: .ascii) {
|
||||
let tokenString = String(data: token, encoding: .ascii)
|
||||
{
|
||||
self.searchPartyToken = tokenString
|
||||
return
|
||||
}
|
||||
@@ -330,7 +346,7 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask to install and activate the mail plugin
|
||||
/// Ask to install and activate the mail plugin.
|
||||
func installMailPlugin() {
|
||||
let pluginManager = MailPluginManager()
|
||||
guard pluginManager.isMailPluginInstalled == false else {
|
||||
@@ -338,7 +354,7 @@ struct OpenHaystackMainView: View {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try pluginManager.installMailPlugin()
|
||||
try pluginManager.installMailPlugin()
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.alertType = .pluginInstallFailed
|
||||
@@ -371,8 +387,8 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
completion?(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +402,8 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
// MARK: - Alerts
|
||||
|
||||
/// Create an alert for the given alert type
|
||||
/// Create an alert for the given alert type.
|
||||
///
|
||||
/// - Parameter alertType: current alert type
|
||||
/// - Returns: A SwiftUI Alert
|
||||
func alert(for alertType: AlertType) -> Alert {
|
||||
@@ -394,51 +411,60 @@ struct OpenHaystackMainView: View {
|
||||
case .keyError:
|
||||
return Alert(title: Text("Could not create accessory"), message: Text(String(describing: self.errorDescription)), dismissButton: Alert.Button.cancel())
|
||||
case .searchPartyToken:
|
||||
return Alert(title: Text("Add the search party token"),
|
||||
message: Text(
|
||||
"""
|
||||
Please paste the search party token below after copying itfrom the macOS Keychain.
|
||||
The item that contains the key can be found by searching for:
|
||||
com.apple.account.DeviceLocator.search-party-token
|
||||
"""
|
||||
),
|
||||
dismissButton: Alert.Button.okay())
|
||||
return Alert(
|
||||
title: Text("Add the search party token"),
|
||||
message: Text(
|
||||
"""
|
||||
Please paste the search party token below after copying itfrom the macOS Keychain.
|
||||
The item that contains the key can be found by searching for:
|
||||
com.apple.account.DeviceLocator.search-party-token
|
||||
"""
|
||||
),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .deployFailed:
|
||||
return Alert(title: Text("Could not deploy"),
|
||||
message: Text("Deploying to microbit failed. Please reconnect the device over USB"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
return Alert(
|
||||
title: Text("Could not deploy"),
|
||||
message: Text("Deploying to microbit failed. Please reconnect the device over USB"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .deployedSuccessfully:
|
||||
return Alert(title: Text("Deploy successfull"),
|
||||
message: Text("This device will now be tracked by all iPhones and you can use this app to find its last reported location"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
return Alert(
|
||||
title: Text("Deploy successfull"),
|
||||
message: Text("This device will now be tracked by all iPhones and you can use this app to find its last reported location"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .deletionFailed:
|
||||
return Alert(title: Text("Could not delete accessory"), dismissButton: Alert.Button.okay())
|
||||
|
||||
case .noReportsFound:
|
||||
return Alert(title: Text("No reports found"),
|
||||
message: Text("Your accessory might have not been found yet or it is not powered. Make sure it has enough power to be found by nearby iPhones"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
return Alert(
|
||||
title: Text("No reports found"),
|
||||
message: Text("Your accessory might have not been found yet or it is not powered. Make sure it has enough power to be found by nearby iPhones"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .activatePlugin:
|
||||
let message =
|
||||
"""
|
||||
To access your Apple ID for downloading location reports we need to use a plugin in Apple Mail.
|
||||
Please make sure Apple Mail is running.
|
||||
Open Mail -> Preferences -> General -> Manage Plug-Ins... -> Select Haystack
|
||||
"""
|
||||
To access your Apple ID for downloading location reports we need to use a plugin in Apple Mail.
|
||||
Please make sure Apple Mail is running.
|
||||
Open Mail -> Preferences -> General -> Manage Plug-Ins... -> Select Haystack
|
||||
|
||||
We do not access any of your e-mail data. This is just necessary, because Apple blocks access to certain iCloud tokens otherwise.
|
||||
"""
|
||||
We do not access any of your e-mail data. This is just necessary, because Apple blocks access to certain iCloud tokens otherwise.
|
||||
"""
|
||||
|
||||
return Alert(title: Text("Install & Activate Mail Plugin"), message: Text(message),
|
||||
primaryButton: .default(Text("Okay"), action: {self.installMailPlugin()}),
|
||||
secondaryButton: .cancel())
|
||||
return Alert(
|
||||
title: Text("Install & Activate Mail Plugin"), message: Text(message),
|
||||
primaryButton: .default(Text("Okay"), action: { self.installMailPlugin() }),
|
||||
secondaryButton: .cancel())
|
||||
|
||||
case .pluginInstallFailed:
|
||||
return Alert(title: Text("Mail Plugin installation failed"),
|
||||
message: Text("To access the location reports of your devices an Apple Mail plugin is necessary" +
|
||||
"\nThe installtion of this plugin has failed.\n\n Please download it manually unzip it and move it to /Library/Mail/Bundles"),
|
||||
primaryButton: .default(Text("Download plug-in"), action: {
|
||||
self.downloadPlugin()
|
||||
}), secondaryButton: .cancel())
|
||||
return Alert(
|
||||
title: Text("Mail Plugin installation failed"),
|
||||
message: Text(
|
||||
"To access the location reports of your devices an Apple Mail plugin is necessary"
|
||||
+ "\nThe installtion of this plugin has failed.\n\n Please download it manually unzip it and move it to /Library/Mail/Bundles"),
|
||||
primaryButton: .default(
|
||||
Text("Download plug-in"),
|
||||
action: {
|
||||
self.downloadPlugin()
|
||||
}), secondaryButton: .cancel())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,9 @@ struct PopUpAlertView: View {
|
||||
}
|
||||
|
||||
}
|
||||
.background(RoundedRectangle(cornerRadius: 7.5)
|
||||
.fill(Color.gray))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 7.5)
|
||||
.fill(Color.gray))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user