Compare commits
4 Commits
f599cb790b
...
5f9af35542
Author | SHA1 | Date | |
---|---|---|---|
|
5f9af35542 | ||
|
e5ea8c4951 | ||
|
f451715a11 | ||
|
32b4c8c81a |
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "sesame.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "watchos",
|
"platform" : "watchos",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@ -27,65 +28,43 @@ struct ContentView: View {
|
|||||||
@State
|
@State
|
||||||
var state: ClientState = .noKeyAvailable
|
var state: ClientState = .noKeyAvailable
|
||||||
|
|
||||||
@State
|
|
||||||
private var hasActiveRequest = false
|
|
||||||
|
|
||||||
let server = Client()
|
let server = Client()
|
||||||
|
|
||||||
|
let history = HistoryManager()
|
||||||
|
|
||||||
var buttonBackground: Color {
|
var buttonBackground: Color {
|
||||||
state.allowsAction ?
|
state.allowsAction ?
|
||||||
.white.opacity(0.2) :
|
.white.opacity(0.2) :
|
||||||
.black.opacity(0.2)
|
.black.opacity(0.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonBorderWidth: CGFloat = 3
|
|
||||||
|
|
||||||
var buttonColor: Color {
|
var buttonColor: Color {
|
||||||
state.allowsAction ? .white : .gray
|
state.allowsAction ? .white : .gray
|
||||||
}
|
}
|
||||||
|
|
||||||
private let sidePaddingRatio: CGFloat = 0.05
|
|
||||||
private let buttonSizeRatio: CGFloat = 0.9
|
|
||||||
|
|
||||||
private let smallButtonHeight: CGFloat = 50
|
|
||||||
|
|
||||||
private let smallButtonWidth: CGFloat = 120
|
|
||||||
|
|
||||||
private let smallButtonBorderWidth: CGFloat = 1
|
|
||||||
|
|
||||||
var compensationTime: UInt32 {
|
|
||||||
isCompensatingDaylightTime ? 3600 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var isPerformingRequests: Bool {
|
|
||||||
hasActiveRequest ||
|
|
||||||
state == .waitingForResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
Spacer()
|
Image(systemSymbol: .lock)
|
||||||
GeometryReader { geo in
|
.resizable()
|
||||||
HStack(alignment: .center) {
|
.aspectRatio(contentMode: .fit)
|
||||||
Spacer()
|
.fontWeight(.ultraLight)
|
||||||
let buttonWidth = min(geo.size.width, geo.size.height)
|
.padding()
|
||||||
Text(state.actionText)
|
|
||||||
.frame(width: buttonWidth, height: buttonWidth)
|
|
||||||
.background(buttonBackground)
|
|
||||||
.cornerRadius(buttonWidth / 2)
|
|
||||||
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2)
|
|
||||||
.stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
|
|
||||||
.foregroundColor(buttonColor)
|
|
||||||
.font(.title)
|
|
||||||
.disabled(!state.allowsAction)
|
|
||||||
.onTapGesture(perform: mainButtonPressed)
|
.onTapGesture(perform: mainButtonPressed)
|
||||||
Spacer()
|
.disabled(!state.allowsAction)
|
||||||
}
|
Text(state.actionText)
|
||||||
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.background(state.color)
|
.background(state.color)
|
||||||
.animation(.easeInOut, value: state.color)
|
.animation(.easeInOut, value: state.color)
|
||||||
|
.onAppear {
|
||||||
|
if keyManager.hasAllKeys, state == .noKeyAvailable {
|
||||||
|
state = .ready
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mainButtonPressed() {
|
func mainButtonPressed() {
|
||||||
@ -97,8 +76,9 @@ struct ContentView: View {
|
|||||||
let count = UInt32(nextMessageCounter)
|
let count = UInt32(nextMessageCounter)
|
||||||
let sentTime = Date()
|
let sentTime = Date()
|
||||||
// Add time to compensate that the device is using daylight savings time
|
// Add time to compensate that the device is using daylight savings time
|
||||||
|
let timeCompensation: UInt32 = isCompensatingDaylightTime ? 3600 : 0
|
||||||
let content = Message.Content(
|
let content = Message.Content(
|
||||||
time: sentTime.timestamp + compensationTime,
|
time: sentTime.timestamp + timeCompensation,
|
||||||
id: count,
|
id: count,
|
||||||
device: deviceId)
|
device: deviceId)
|
||||||
let message = content.authenticate(using: key)
|
let message = content.authenticate(using: key)
|
||||||
@ -138,7 +118,11 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func save(historyItem: HistoryItem) {
|
private func save(historyItem: HistoryItem) {
|
||||||
|
do {
|
||||||
|
try history.save(item: historyItem)
|
||||||
|
} catch {
|
||||||
|
print("Failed to save item: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
67
Sesame-Watch Watch App/HistoryItemDetail.swift
Normal file
67
Sesame-Watch Watch App/HistoryItemDetail.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .short
|
||||||
|
df.doesRelativeDateFormatting = true
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
struct HistoryItemDetail: View {
|
||||||
|
|
||||||
|
let item: HistoryItem
|
||||||
|
|
||||||
|
private var entryTime: String {
|
||||||
|
df.string(from: item.requestDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var counterText: String {
|
||||||
|
let sentCounter = item.request.id
|
||||||
|
let startText = "\(sentCounter)"
|
||||||
|
guard let rCounter = item.responseMessage?.id else {
|
||||||
|
return startText
|
||||||
|
}
|
||||||
|
guard sentCounter + 1 != rCounter && sentCounter != rCounter else {
|
||||||
|
return startText
|
||||||
|
}
|
||||||
|
return "\(sentCounter) -> \(rCounter)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Status",
|
||||||
|
value: item.response?.description ?? "No response")
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Date",
|
||||||
|
value: entryTime)
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Connection",
|
||||||
|
value: item.usedLocalConnection ? "Local" : "Remote")
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Device ID",
|
||||||
|
value: "\(item.request.deviceId!)")
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Message Counter",
|
||||||
|
value: counterText)
|
||||||
|
if let time = item.roundTripTime {
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Round Trip Time",
|
||||||
|
value: "\(Int(time * 1000)) ms")
|
||||||
|
}
|
||||||
|
if let offset = item.clockOffset {
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Clock offset",
|
||||||
|
value: "\(offset) seconds")
|
||||||
|
}
|
||||||
|
}.navigationTitle("Details")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HistoryItemDetail_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
HistoryItemDetail(item: .mock)
|
||||||
|
}
|
||||||
|
}
|
41
Sesame-Watch Watch App/HistoryListRow.swift
Normal file
41
Sesame-Watch Watch App/HistoryListRow.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .short
|
||||||
|
df.doesRelativeDateFormatting = true
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
struct HistoryListRow: View {
|
||||||
|
|
||||||
|
let item: HistoryItem
|
||||||
|
|
||||||
|
private var entryTime: String {
|
||||||
|
df.string(from: item.requestDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: item.response?.symbol ?? .exclamationmarkTriangle)
|
||||||
|
Text(item.response?.description ?? "No response")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
Text(entryTime)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HistoryListRow_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
HistoryListRow(item: .mock)
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,37 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HistoryView: View {
|
struct HistoryView: View {
|
||||||
|
|
||||||
|
let history: HistoryManagerProtocol
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var items: [HistoryItem] = []
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
NavigationStack {
|
||||||
|
List(items) { item in
|
||||||
|
NavigationLink {
|
||||||
|
HistoryItemDetail(item: item)
|
||||||
|
} label: {
|
||||||
|
HistoryListRow(item: item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("History")
|
||||||
|
}.onAppear(perform: loadItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadItems() {
|
||||||
|
Task {
|
||||||
|
let entries = history.loadEntries()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
items = entries
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HistoryView_Previews: PreviewProvider {
|
struct HistoryView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
HistoryView()
|
HistoryView(history: HistoryManagerMock())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,121 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CryptoKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
private let localKey: [UInt8] = [
|
|
||||||
0x98, 0x36, 0x91, 0x09, 0x29, 0xa0, 0x54, 0x44,
|
|
||||||
0x03, 0x0c, 0xa5, 0xb4, 0x20, 0x16, 0x10, 0x0d,
|
|
||||||
0xaf, 0x41, 0x9b, 0x26, 0x4f, 0x75, 0xa4, 0x61,
|
|
||||||
0xed, 0x15, 0x0c, 0xb3, 0x06, 0x39, 0x92, 0x59]
|
|
||||||
|
|
||||||
|
|
||||||
private let remoteKey: [UInt8] = [
|
|
||||||
0xfa, 0x23, 0xf6, 0x98, 0xea, 0x87, 0x23, 0xa0,
|
|
||||||
0xa0, 0xbe, 0x9a, 0xdb, 0x31, 0x28, 0xcb, 0x7d,
|
|
||||||
0xd3, 0xa5, 0x7b, 0xf0, 0xc0, 0xeb, 0x45, 0x65,
|
|
||||||
0x4d, 0x94, 0x50, 0x1a, 0x2f, 0x6f, 0xeb, 0x70]
|
|
||||||
|
|
||||||
private let authToken: [UInt8] = {
|
|
||||||
let s = "Y6QzDK5DaFK1w2oEX5OkzoC0nTqP8w5IxpvWAR1mpro="
|
|
||||||
let t = Data(base64Encoded: s.data(using: .utf8)!)!
|
|
||||||
return Array(t)
|
|
||||||
}()
|
|
||||||
|
|
||||||
extension KeyManagement {
|
|
||||||
|
|
||||||
enum KeyType: String, Identifiable, CaseIterable {
|
|
||||||
|
|
||||||
case deviceKey = "sesame-device"
|
|
||||||
case remoteKey = "sesame-remote"
|
|
||||||
case authToken = "sesame-remote-auth"
|
|
||||||
|
|
||||||
var id: String {
|
|
||||||
rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayName: String {
|
|
||||||
switch self {
|
|
||||||
case .deviceKey:
|
|
||||||
return "Device Key"
|
|
||||||
case .remoteKey:
|
|
||||||
return "Remote Key"
|
|
||||||
case .authToken:
|
|
||||||
return "Authentication Token"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyLength: SymmetricKeySize {
|
|
||||||
.bits256
|
|
||||||
}
|
|
||||||
|
|
||||||
var usesHashing: Bool {
|
|
||||||
switch self {
|
|
||||||
case .authToken:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension KeyManagement.KeyType: CustomStringConvertible {
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
displayName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class KeyManagement: ObservableObject {
|
|
||||||
|
|
||||||
|
|
||||||
@Published
|
|
||||||
private(set) var hasRemoteKey = true
|
|
||||||
|
|
||||||
@Published
|
|
||||||
private(set) var hasDeviceKey = true
|
|
||||||
|
|
||||||
@Published
|
|
||||||
private(set) var hasAuthToken = true
|
|
||||||
|
|
||||||
var hasAllKeys: Bool {
|
|
||||||
hasRemoteKey && hasDeviceKey && hasAuthToken
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {}
|
|
||||||
|
|
||||||
func has(_ type: KeyType) -> Bool {
|
|
||||||
switch type {
|
|
||||||
case .deviceKey:
|
|
||||||
return hasDeviceKey
|
|
||||||
case .remoteKey:
|
|
||||||
return hasRemoteKey
|
|
||||||
case .authToken:
|
|
||||||
return hasAuthToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func get(_ type: KeyType) -> SymmetricKey? {
|
|
||||||
let bytes: [UInt8] = get(type)
|
|
||||||
return SymmetricKey(data: bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func get(_ type: KeyType) -> [UInt8] {
|
|
||||||
switch type {
|
|
||||||
case .deviceKey:
|
|
||||||
return remoteKey
|
|
||||||
case .remoteKey:
|
|
||||||
return localKey
|
|
||||||
case .authToken:
|
|
||||||
return authToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func delete(_ type: KeyType) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func generate(_ type: KeyType) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,7 +11,8 @@ struct Sesame_Watch_Watch_AppApp: App {
|
|||||||
ContentView()
|
ContentView()
|
||||||
.environmentObject(keyManagement)
|
.environmentObject(keyManagement)
|
||||||
SettingsView()
|
SettingsView()
|
||||||
HistoryView()
|
.environmentObject(keyManagement)
|
||||||
|
HistoryView(history: HistoryManager())
|
||||||
}
|
}
|
||||||
.tabViewStyle(PageTabViewStyle())
|
.tabViewStyle(PageTabViewStyle())
|
||||||
}
|
}
|
||||||
|
77
Sesame-Watch Watch App/Settings/SettingsKeyInputView.swift
Normal file
77
Sesame-Watch Watch App/Settings/SettingsKeyInputView.swift
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
struct SettingsKeyInputView: View {
|
||||||
|
|
||||||
|
let type: KeyManagement.KeyType
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var text: String = ""
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var keys: KeyManagement
|
||||||
|
|
||||||
|
private var hasKey: Bool {
|
||||||
|
keys.has(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var displayText: String {
|
||||||
|
keys.get(type)?.displayString ?? "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var copyText: String {
|
||||||
|
guard let key = keys.get(type)?.data else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
guard type.usesHashing else {
|
||||||
|
return key.hexEncoded
|
||||||
|
}
|
||||||
|
return SHA256.hash(data: key).hexEncoded
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TextField(type.displayName, text: $text)
|
||||||
|
.onSubmit(validateText)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text(footnote)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.navigationTitle(type.displayName)
|
||||||
|
.onAppear {
|
||||||
|
if text == "" {
|
||||||
|
text = displayText
|
||||||
|
print("Text inserted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateText() {
|
||||||
|
let cleanText = text.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
guard let keyData = Data(fromHexEncodedString: cleanText) else {
|
||||||
|
print("Invalid key string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let keyLength = type.keyLength.bitCount
|
||||||
|
guard keyData.count * 8 == keyLength else {
|
||||||
|
print("Invalid key length \(keyData.count * 8) bits, expected \(keyLength) (Input: '\(text)')")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keys.save(type, data: keyData)
|
||||||
|
print("Key \(type) saved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsKeyInputView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsKeyInputView(
|
||||||
|
type: .remoteKey,
|
||||||
|
footnote: "Some text describing the purpose of the key.")
|
||||||
|
.environmentObject(KeyManagement())
|
||||||
|
}
|
||||||
|
}
|
47
Sesame-Watch Watch App/Settings/SettingsKeyItemLink.swift
Normal file
47
Sesame-Watch Watch App/Settings/SettingsKeyItemLink.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsKeyItemLink: View {
|
||||||
|
|
||||||
|
let type: KeyManagement.KeyType
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var keys: KeyManagement
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var keyText = "..."
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink {
|
||||||
|
SettingsKeyInputView(
|
||||||
|
type: type,
|
||||||
|
footnote: footnote)
|
||||||
|
.environmentObject(keys)
|
||||||
|
} label: {
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: type.displayName,
|
||||||
|
value: keyText)
|
||||||
|
.onAppear(perform: updateKeyText)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateKeyText() {
|
||||||
|
Task {
|
||||||
|
let key = keys.get(type)?.displayString ?? "Not set"
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
keyText = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsKeyItemLink_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsKeyItemLink(
|
||||||
|
type: .deviceKey,
|
||||||
|
footnote: "Some text describing the purpose of the key.")
|
||||||
|
.environmentObject(KeyManagement())
|
||||||
|
}
|
||||||
|
}
|
31
Sesame-Watch Watch App/Settings/SettingsListTextItem.swift
Normal file
31
Sesame-Watch Watch App/Settings/SettingsListTextItem.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsListTextItem: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Text(title)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Text(value)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsListTextItem_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Title",
|
||||||
|
value: "Some longer text")
|
||||||
|
}
|
||||||
|
}
|
28
Sesame-Watch Watch App/Settings/SettingsListToggleItem.swift
Normal file
28
Sesame-Watch Watch App/Settings/SettingsListToggleItem.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsListToggleItem: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var value: Bool
|
||||||
|
|
||||||
|
let subtitle: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Toggle(title, isOn: $value)
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsListToggleItem_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsListToggleItem(title: "Toggle", value: .constant(true), subtitle: "Some longer text explaining what the toggle does")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsNumberInputView: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var value: Int
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var text: String = ""
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TextField(title, text: $text)
|
||||||
|
.onSubmit {
|
||||||
|
guard let newValue = Int(text) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value = newValue
|
||||||
|
}
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text(footnote)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.navigationTitle(title)
|
||||||
|
.navigationBarBackButtonHidden(false)
|
||||||
|
.onAppear {
|
||||||
|
text = "\(value)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsNumberInputView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsNumberInputView(
|
||||||
|
title: "Title",
|
||||||
|
value: .constant(0),
|
||||||
|
footnote: "Some more text explaining the purpose of the text field.")
|
||||||
|
}
|
||||||
|
}
|
30
Sesame-Watch Watch App/Settings/SettingsNumberItemLink.swift
Normal file
30
Sesame-Watch Watch App/Settings/SettingsNumberItemLink.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsNumberItemLink: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var value: Int
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink {
|
||||||
|
SettingsNumberInputView(
|
||||||
|
title: title,
|
||||||
|
value: $value,
|
||||||
|
footnote: footnote
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
SettingsListTextItem(title: title, value: "\(value)")
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsNumberItemLink_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsNumberItemLink(title: "Title", value: .constant(0), footnote: "Some more text explaining the purpose of the text field.")
|
||||||
|
}
|
||||||
|
}
|
33
Sesame-Watch Watch App/Settings/SettingsTextInputView.swift
Normal file
33
Sesame-Watch Watch App/Settings/SettingsTextInputView.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsTextInputView: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var text: String
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TextField(title, text: $text)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text(footnote)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.navigationTitle(title)
|
||||||
|
.navigationBarBackButtonHidden(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsTextInputView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsTextInputView(
|
||||||
|
title: "Title",
|
||||||
|
text: .constant("Text"),
|
||||||
|
footnote: "Some more text explaining the purpose of the text field.")
|
||||||
|
}
|
||||||
|
}
|
30
Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift
Normal file
30
Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsTextItemLink: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var value: String
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink {
|
||||||
|
SettingsTextInputView(
|
||||||
|
title: title,
|
||||||
|
text: $value,
|
||||||
|
footnote: footnote
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
SettingsListTextItem(title: title, value: value)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsTextItemLink_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsTextItemLink(title: "Title", value: .constant("Some value"), footnote: "Some more text explaining the purpose of the text field.")
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,77 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
|
||||||
|
@AppStorage("server")
|
||||||
|
var serverPath: String = "https://christophhagen.de/sesame/"
|
||||||
|
|
||||||
|
@AppStorage("localIP")
|
||||||
|
var localAddress: String = "192.168.178.104/"
|
||||||
|
|
||||||
|
@AppStorage("counter")
|
||||||
|
var nextMessageCounter: Int = 0
|
||||||
|
|
||||||
|
@AppStorage("compensate")
|
||||||
|
var isCompensatingDaylightTime: Bool = false
|
||||||
|
|
||||||
|
@AppStorage("local")
|
||||||
|
private var useLocalConnection = false
|
||||||
|
|
||||||
|
@AppStorage("deviceId")
|
||||||
|
private var deviceId: Int = 0
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var keys: KeyManagement
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
NavigationStack {
|
||||||
VStack {
|
List {
|
||||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
SettingsTextItemLink(
|
||||||
}
|
title: "Server url",
|
||||||
|
value: $serverPath,
|
||||||
|
footnote: "The url where the sesame server listens for incoming messages.")
|
||||||
|
SettingsTextItemLink(
|
||||||
|
title: "Local url",
|
||||||
|
value: $localAddress,
|
||||||
|
footnote: "The url where the device can be reached directly on the local WiFi network.")
|
||||||
|
SettingsListToggleItem(
|
||||||
|
title: "Local connection",
|
||||||
|
value: $useLocalConnection,
|
||||||
|
subtitle: "Attempt to communicate directly with the device, which requires a WiFi connection on the same network.")
|
||||||
|
SettingsNumberItemLink(
|
||||||
|
title: "Device ID",
|
||||||
|
value: $deviceId,
|
||||||
|
footnote: "The device ID is unique for each remote device, and is assigned by the system administrator.")
|
||||||
|
SettingsNumberItemLink(
|
||||||
|
title: "Message counter",
|
||||||
|
value: $nextMessageCounter,
|
||||||
|
footnote: "The message counter is increased after every message to the device, and used to prevent replay attacks.")
|
||||||
|
SettingsListToggleItem(
|
||||||
|
title: "Daylight savings",
|
||||||
|
value: $isCompensatingDaylightTime,
|
||||||
|
subtitle: "Compensate timestamps if the remote has daylight savings time wrongly set.")
|
||||||
|
SettingsKeyItemLink(
|
||||||
|
type: .deviceKey,
|
||||||
|
footnote: "Some text describing the purpose of the key.")
|
||||||
|
.environmentObject(keys)
|
||||||
|
SettingsKeyItemLink(
|
||||||
|
type: .remoteKey,
|
||||||
|
footnote: "Some text describing the purpose of the key.")
|
||||||
|
.environmentObject(keys)
|
||||||
|
SettingsKeyItemLink(
|
||||||
|
type: .authToken,
|
||||||
|
footnote: "Some text describing the purpose of the key.")
|
||||||
|
.environmentObject(keys)
|
||||||
}
|
}
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SettingsView_Previews: PreviewProvider {
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
.previewDevice("Apple Watch Series 7 - 41mm")
|
.previewDevice("Apple Watch Series 7 - 41mm")
|
||||||
|
.environmentObject(KeyManagement())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* ContentView.swift */; };
|
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* ContentView.swift */; };
|
||||||
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; };
|
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; };
|
||||||
88E197B929EDC9BD00BF1D19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */; };
|
88E197B929EDC9BD00BF1D19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */; };
|
||||||
88E197C229EDCB0900BF1D19 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197C129EDCB0900BF1D19 /* KeyManagement.swift */; };
|
|
||||||
88E197C429EDCC8900BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
88E197C429EDCC8900BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
||||||
88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
||||||
88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; };
|
88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; };
|
||||||
@ -39,6 +38,18 @@
|
|||||||
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */; };
|
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */; };
|
||||||
88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
|
88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
|
||||||
88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
|
88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
|
||||||
|
E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */; };
|
||||||
|
E240654D2A8155A3009C1AD8 /* SettingsListToggleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */; };
|
||||||
|
E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */; };
|
||||||
|
E24065512A819066009C1AD8 /* SettingsTextItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */; };
|
||||||
|
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */; };
|
||||||
|
E24065552A819663009C1AD8 /* SettingsNumberInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */; };
|
||||||
|
E24065582A819AE3009C1AD8 /* SettingsKeyItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */; };
|
||||||
|
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */; };
|
||||||
|
E240655B2A822397009C1AD8 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
|
||||||
|
E240655C2A822C8E009C1AD8 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED36281EC7FB00259690 /* HistoryManager.swift */; };
|
||||||
|
E240655E2A822E97009C1AD8 /* HistoryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240655D2A822E97009C1AD8 /* HistoryListRow.swift */; };
|
||||||
|
E24065602A822ED9009C1AD8 /* HistoryItemDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */; };
|
||||||
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */; };
|
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */; };
|
||||||
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; };
|
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; };
|
||||||
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
|
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
|
||||||
@ -72,8 +83,17 @@
|
|||||||
88E197B329EDC9BC00BF1D19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
88E197B329EDC9BC00BF1D19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
88E197C129EDCB0900BF1D19 /* KeyManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagement.swift; sourceTree = "<group>"; };
|
|
||||||
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
|
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListTextItem.swift; sourceTree = "<group>"; };
|
||||||
|
E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListToggleItem.swift; sourceTree = "<group>"; };
|
||||||
|
E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTextInputView.swift; sourceTree = "<group>"; };
|
||||||
|
E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTextItemLink.swift; sourceTree = "<group>"; };
|
||||||
|
E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNumberItemLink.swift; sourceTree = "<group>"; };
|
||||||
|
E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNumberInputView.swift; sourceTree = "<group>"; };
|
||||||
|
E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyItemLink.swift; sourceTree = "<group>"; };
|
||||||
|
E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyInputView.swift; sourceTree = "<group>"; };
|
||||||
|
E240655D2A822E97009C1AD8 /* HistoryListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListRow.swift; sourceTree = "<group>"; };
|
||||||
|
E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItemDetail.swift; sourceTree = "<group>"; };
|
||||||
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = "<group>"; };
|
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = "<group>"; };
|
||||||
E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
||||||
@ -166,11 +186,13 @@
|
|||||||
88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = {
|
88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E24065562A819AAD009C1AD8 /* Settings */,
|
||||||
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */,
|
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */,
|
||||||
88E197B329EDC9BC00BF1D19 /* ContentView.swift */,
|
88E197B329EDC9BC00BF1D19 /* ContentView.swift */,
|
||||||
888362332A80F3F90032BBB2 /* SettingsView.swift */,
|
888362332A80F3F90032BBB2 /* SettingsView.swift */,
|
||||||
888362352A80F4420032BBB2 /* HistoryView.swift */,
|
888362352A80F4420032BBB2 /* HistoryView.swift */,
|
||||||
88E197C129EDCB0900BF1D19 /* KeyManagement.swift */,
|
E240655D2A822E97009C1AD8 /* HistoryListRow.swift */,
|
||||||
|
E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */,
|
||||||
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */,
|
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */,
|
||||||
88E197B729EDC9BD00BF1D19 /* Preview Content */,
|
88E197B729EDC9BD00BF1D19 /* Preview Content */,
|
||||||
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */,
|
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */,
|
||||||
@ -193,6 +215,21 @@
|
|||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E24065562A819AAD009C1AD8 /* Settings */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */,
|
||||||
|
E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */,
|
||||||
|
E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */,
|
||||||
|
E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */,
|
||||||
|
E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */,
|
||||||
|
E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */,
|
||||||
|
E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */,
|
||||||
|
E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */,
|
||||||
|
);
|
||||||
|
path = Settings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E2C5C1D92806FE4A00769EF6 /* API */ = {
|
E2C5C1D92806FE4A00769EF6 /* API */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -351,20 +388,31 @@
|
|||||||
files = (
|
files = (
|
||||||
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */,
|
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */,
|
||||||
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */,
|
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */,
|
||||||
|
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */,
|
||||||
888362362A80F4420032BBB2 /* HistoryView.swift in Sources */,
|
888362362A80F4420032BBB2 /* HistoryView.swift in Sources */,
|
||||||
|
E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */,
|
||||||
88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */,
|
88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */,
|
||||||
88E197D529EDCE8800BF1D19 /* ServerMessage.swift in Sources */,
|
88E197D529EDCE8800BF1D19 /* ServerMessage.swift in Sources */,
|
||||||
88E197D129EDCE5F00BF1D19 /* Data+Extensions.swift in Sources */,
|
88E197D129EDCE5F00BF1D19 /* Data+Extensions.swift in Sources */,
|
||||||
|
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */,
|
||||||
88E197D229EDCE6600BF1D19 /* RouteAPI.swift in Sources */,
|
88E197D229EDCE6600BF1D19 /* RouteAPI.swift in Sources */,
|
||||||
|
E24065512A819066009C1AD8 /* SettingsTextItemLink.swift in Sources */,
|
||||||
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */,
|
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */,
|
||||||
88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */,
|
88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */,
|
||||||
|
E24065602A822ED9009C1AD8 /* HistoryItemDetail.swift in Sources */,
|
||||||
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */,
|
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */,
|
||||||
|
E24065582A819AE3009C1AD8 /* SettingsKeyItemLink.swift in Sources */,
|
||||||
88E197C929EDCCE100BF1D19 /* Message.swift in Sources */,
|
88E197C929EDCCE100BF1D19 /* Message.swift in Sources */,
|
||||||
88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */,
|
88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */,
|
||||||
|
E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */,
|
||||||
88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */,
|
88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */,
|
||||||
88E197D429EDCE7600BF1D19 /* UInt32+Extensions.swift in Sources */,
|
88E197D429EDCE7600BF1D19 /* UInt32+Extensions.swift in Sources */,
|
||||||
|
E240655B2A822397009C1AD8 /* KeyManagement.swift in Sources */,
|
||||||
|
E240654D2A8155A3009C1AD8 /* SettingsListToggleItem.swift in Sources */,
|
||||||
|
E24065552A819663009C1AD8 /* SettingsNumberInputView.swift in Sources */,
|
||||||
|
E240655E2A822E97009C1AD8 /* HistoryListRow.swift in Sources */,
|
||||||
88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */,
|
88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */,
|
||||||
88E197C229EDCB0900BF1D19 /* KeyManagement.swift in Sources */,
|
E240655C2A822C8E009C1AD8 /* HistoryManager.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
Binary file not shown.
@ -67,11 +67,16 @@
|
|||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Sesame.xcscheme_^#shared#^_</key>
|
<key>Sesame-Watch Watch App.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>Sesame.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -27,8 +27,8 @@ struct DeviceResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shorthand property for an invalid message.
|
/// Shorthand property for an invalid message.
|
||||||
static var invalidMessageData: DeviceResponse {
|
static var invalidMessageSize: DeviceResponse {
|
||||||
.init(event: .invalidMessageData)
|
.init(event: .invalidMessageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorthand property for missing body data.
|
/// Shorthand property for missing body data.
|
||||||
@ -83,8 +83,8 @@ struct DeviceResponse {
|
|||||||
/// Get the reponse encoded in bytes.
|
/// Get the reponse encoded in bytes.
|
||||||
var encoded: Data {
|
var encoded: Data {
|
||||||
guard let message = response else {
|
guard let message = response else {
|
||||||
return Data([event.rawValue])
|
return event.encoded
|
||||||
}
|
}
|
||||||
return Data([event.rawValue]) + message.encoded
|
return event.encoded + message.encoded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,8 @@ enum MessageResult: UInt8 {
|
|||||||
/// A socket event on the device was unexpected (not binary data)
|
/// A socket event on the device was unexpected (not binary data)
|
||||||
case unexpectedSocketEvent = 2
|
case unexpectedSocketEvent = 2
|
||||||
|
|
||||||
/// The size of the payload (i.e. message) was invalid, or the data could not be read
|
/// The size of the payload (i.e. message) was invalid
|
||||||
case invalidMessageData = 3
|
case invalidMessageSize = 3
|
||||||
|
|
||||||
/// The transmitted message could not be authenticated using the key
|
/// The transmitted message could not be authenticated using the key
|
||||||
case messageAuthenticationFailed = 4
|
case messageAuthenticationFailed = 4
|
||||||
@ -44,6 +44,10 @@ enum MessageResult: UInt8 {
|
|||||||
|
|
||||||
/// The device is connected
|
/// The device is connected
|
||||||
case deviceConnected = 15
|
case deviceConnected = 15
|
||||||
|
|
||||||
|
case invalidUrlParameter = 20
|
||||||
|
|
||||||
|
case invalidResponseAuthentication = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MessageResult: CustomStringConvertible {
|
extension MessageResult: CustomStringConvertible {
|
||||||
@ -54,7 +58,7 @@ extension MessageResult: CustomStringConvertible {
|
|||||||
return "The device received unexpected text"
|
return "The device received unexpected text"
|
||||||
case .unexpectedSocketEvent:
|
case .unexpectedSocketEvent:
|
||||||
return "Unexpected socket event for the device"
|
return "Unexpected socket event for the device"
|
||||||
case .invalidMessageData:
|
case .invalidMessageSize:
|
||||||
return "Invalid message data"
|
return "Invalid message data"
|
||||||
case .messageAuthenticationFailed:
|
case .messageAuthenticationFailed:
|
||||||
return "Message authentication failed"
|
return "Message authentication failed"
|
||||||
@ -76,6 +80,17 @@ extension MessageResult: CustomStringConvertible {
|
|||||||
return "Another operation is in progress"
|
return "Another operation is in progress"
|
||||||
case .deviceConnected:
|
case .deviceConnected:
|
||||||
return "The device is connected"
|
return "The device is connected"
|
||||||
|
case .invalidUrlParameter:
|
||||||
|
return "The url parameter could not be found"
|
||||||
|
case .invalidResponseAuthentication:
|
||||||
|
return "The response could not be authenticated"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MessageResult {
|
||||||
|
|
||||||
|
var encoded: Data {
|
||||||
|
Data([rawValue])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,8 +11,6 @@ struct ServerMessage {
|
|||||||
|
|
||||||
static let authTokenSize = SHA256.byteCount
|
static let authTokenSize = SHA256.byteCount
|
||||||
|
|
||||||
static let length = authTokenSize + Message.length
|
|
||||||
|
|
||||||
let authToken: Data
|
let authToken: Data
|
||||||
|
|
||||||
let message: Message
|
let message: Message
|
||||||
@ -22,30 +20,7 @@ struct ServerMessage {
|
|||||||
self.message = message
|
self.message = message
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
Decode a message from a byte buffer.
|
|
||||||
The buffer must contain at least `ServerMessage.length` bytes, or it will return `nil`.
|
|
||||||
- Parameter buffer: The buffer containing the bytes.
|
|
||||||
*/
|
|
||||||
init?(decodeFrom buffer: ByteBuffer) {
|
|
||||||
guard let data = buffer.getBytes(at: 0, length: ServerMessage.length) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
|
|
||||||
self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.authTokenSize)))
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoded: Data {
|
var encoded: Data {
|
||||||
authToken + message.encoded
|
authToken + message.encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
static func token(from buffer: ByteBuffer) -> Data? {
|
|
||||||
guard buffer.readableBytes == authTokenSize else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let bytes = buffer.getBytes(at: 0, length: authTokenSize) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return Data(bytes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ final class Client {
|
|||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpBody = data
|
request.httpBody = data
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
|
request.timeoutInterval = 10
|
||||||
return await requestAndDecode(request)
|
return await requestAndDecode(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
enum ConnectionError {
|
enum ConnectionError {
|
||||||
case serverNotReached
|
case serverNotReached
|
||||||
@ -97,7 +98,8 @@ enum ClientState {
|
|||||||
self = .openSesame
|
self = .openSesame
|
||||||
case .messageDeviceInvalid:
|
case .messageDeviceInvalid:
|
||||||
self = .messageRejected(.invalidDeviceId)
|
self = .messageRejected(.invalidDeviceId)
|
||||||
case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
|
case .noBodyData, .invalidMessageSize, .textReceived, .unexpectedSocketEvent, .invalidUrlParameter, .invalidResponseAuthentication:
|
||||||
|
print("Unexpected internal error: \(keyResult)")
|
||||||
self = .internalError(keyResult.description)
|
self = .internalError(keyResult.description)
|
||||||
case .deviceNotConnected:
|
case .deviceNotConnected:
|
||||||
self = .deviceNotAvailable(.deviceDisconnected)
|
self = .deviceNotAvailable(.deviceDisconnected)
|
||||||
@ -242,7 +244,7 @@ extension ClientState {
|
|||||||
}
|
}
|
||||||
case .openSesame:
|
case .openSesame:
|
||||||
return 17
|
return 17
|
||||||
case .internalError(_):
|
case .internalError:
|
||||||
return 18
|
return 18
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,3 +296,26 @@ extension ClientState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ClientState {
|
||||||
|
|
||||||
|
@available(iOS 16, *)
|
||||||
|
var symbol: SFSymbol {
|
||||||
|
switch self {
|
||||||
|
case .deviceNotAvailable:
|
||||||
|
return .wifiExclamationmark
|
||||||
|
case .internalError:
|
||||||
|
return .applewatchSlash
|
||||||
|
case .noKeyAvailable:
|
||||||
|
return .lockTrianglebadgeExclamationmark
|
||||||
|
case .openSesame:
|
||||||
|
return .lockOpen
|
||||||
|
case .messageRejected:
|
||||||
|
return .nosign
|
||||||
|
case .responseRejected:
|
||||||
|
return .exclamationmarkTriangle
|
||||||
|
case .requestingStatus, .ready, .waitingForResponse:
|
||||||
|
return .wifiExclamationmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -54,23 +54,19 @@ struct HistoryListItem: View {
|
|||||||
HStack {
|
HStack {
|
||||||
if let roundTripText {
|
if let roundTripText {
|
||||||
Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network)
|
Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network)
|
||||||
//Image(systemSymbol: .arrowUpArrowDownCircle)
|
|
||||||
Text(roundTripText)
|
Text(roundTripText)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
//Spacer()
|
|
||||||
Image(systemSymbol: .personalhotspot)
|
Image(systemSymbol: .personalhotspot)
|
||||||
Text(counterText)
|
Text(counterText)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
if let timeOffsetText {
|
if let timeOffsetText {
|
||||||
//Spacer()
|
|
||||||
Image(systemSymbol: .stopwatch)
|
Image(systemSymbol: .stopwatch)
|
||||||
Text(timeOffsetText)
|
Text(timeOffsetText)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
}
|
}
|
||||||
}.foregroundColor(.secondary)
|
}.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
//.padding()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user