Local route over UDP
This commit is contained in:
parent
877bba56b4
commit
91af68a44b
@ -53,6 +53,7 @@
|
|||||||
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */; };
|
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */; };
|
||||||
88AEE3862B22376D0034EDA9 /* Message+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */; };
|
88AEE3862B22376D0034EDA9 /* Message+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */; };
|
||||||
88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */; };
|
88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */; };
|
||||||
|
88BA7DD32BD41B8A008F2A3C /* UDPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */; };
|
||||||
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */; };
|
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */; };
|
||||||
88E197B429EDC9BC00BF1D19 /* UnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* UnlockView.swift */; };
|
88E197B429EDC9BC00BF1D19 /* UnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* UnlockView.swift */; };
|
||||||
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; };
|
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; };
|
||||||
@ -157,6 +158,7 @@
|
|||||||
88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedMessage.swift; sourceTree = "<group>"; };
|
88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedMessage.swift; sourceTree = "<group>"; };
|
||||||
88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Crypto.swift"; sourceTree = "<group>"; };
|
88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Crypto.swift"; sourceTree = "<group>"; };
|
||||||
88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageResult+UI.swift"; sourceTree = "<group>"; };
|
88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageResult+UI.swift"; sourceTree = "<group>"; };
|
||||||
|
88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPClient.swift; sourceTree = "<group>"; };
|
||||||
88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sesame-Watch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sesame-Watch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sesame_WatchApp.swift; sourceTree = "<group>"; };
|
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sesame_WatchApp.swift; sourceTree = "<group>"; };
|
||||||
88E197B329EDC9BC00BF1D19 /* UnlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockView.swift; sourceTree = "<group>"; };
|
88E197B329EDC9BC00BF1D19 /* UnlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockView.swift; sourceTree = "<group>"; };
|
||||||
@ -354,6 +356,7 @@
|
|||||||
8860D7612B23803E00849FAC /* ServerChallenge.swift */,
|
8860D7612B23803E00849FAC /* ServerChallenge.swift */,
|
||||||
8860D7642B23B5B200849FAC /* RequestCoordinator.swift */,
|
8860D7642B23B5B200849FAC /* RequestCoordinator.swift */,
|
||||||
8860D7672B23D04100849FAC /* PendingOperation.swift */,
|
8860D7672B23D04100849FAC /* PendingOperation.swift */,
|
||||||
|
88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */,
|
||||||
);
|
);
|
||||||
path = Common;
|
path = Common;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -579,6 +582,7 @@
|
|||||||
88AEE3812B22327F0034EDA9 /* UInt32+Random.swift in Sources */,
|
88AEE3812B22327F0034EDA9 /* UInt32+Random.swift in Sources */,
|
||||||
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */,
|
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */,
|
||||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||||
|
88BA7DD32BD41B8A008F2A3C /* UDPClient.swift in Sources */,
|
||||||
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */,
|
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */,
|
||||||
8860D74A2B2329CE00849FAC /* SignedMessage+Size.swift in Sources */,
|
8860D74A2B2329CE00849FAC /* SignedMessage+Size.swift in Sources */,
|
||||||
8860D7542B23489300849FAC /* ActiveRequestType.swift in Sources */,
|
8860D7542B23489300849FAC /* ActiveRequestType.swift in Sources */,
|
||||||
|
@ -3,13 +3,9 @@ import CryptoKit
|
|||||||
|
|
||||||
final class Client {
|
final class Client {
|
||||||
|
|
||||||
private let localRequestRoute = "message"
|
|
||||||
|
|
||||||
private let urlMessageParameter = "m"
|
|
||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
|
|
||||||
func send(_ message: Message, to url: String, through route: TransmissionType, using keys: KeySet) async -> ServerResponse {
|
func send(_ message: Message, to url: String, port: UInt16, through route: TransmissionType, using keys: KeySet) async -> ServerResponse {
|
||||||
let sentTime = Date.now
|
let sentTime = Date.now
|
||||||
let signedMessage = message.authenticate(using: keys.remote)
|
let signedMessage = message.authenticate(using: keys.remote)
|
||||||
let response: Message
|
let response: Message
|
||||||
@ -18,7 +14,7 @@ final class Client {
|
|||||||
response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device)
|
response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device)
|
||||||
|
|
||||||
case .overLocalWifi:
|
case .overLocalWifi:
|
||||||
response = await send(signedMessage, toLocalDeviceUrl: url, verifyUsing: keys.device)
|
response = await send(signedMessage, toLocalDevice: url, port: port, verifyUsing: keys.device)
|
||||||
}
|
}
|
||||||
let receivedTime = Date.now
|
let receivedTime = Date.now
|
||||||
// Create best guess for creation of challenge.
|
// Create best guess for creation of challenge.
|
||||||
@ -40,17 +36,18 @@ final class Client {
|
|||||||
return (response, serverChallenge)
|
return (response, serverChallenge)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func send(_ message: SignedMessage, toLocalDevice host: String, port: UInt16, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||||
private func send(_ message: SignedMessage, toLocalDeviceUrl server: String, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
let client = UDPClient(host: host, port: port)
|
||||||
let data = message.encoded.hexEncoded
|
let response: Data? = await withCheckedContinuation { continuation in
|
||||||
guard let url = URL(string: server)?.appendingPathComponent("\(localRequestRoute)?\(urlMessageParameter)=\(data)") else {
|
client.begin()
|
||||||
return message.message.with(result: .serverUrlInvalid)
|
client.send(message: message.encoded) { res in
|
||||||
|
continuation.resume(returning: res)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
var request = URLRequest(url: url)
|
guard let data = response else {
|
||||||
request.httpMethod = "POST"
|
return message.message.with(result: .deviceNotConnected)
|
||||||
request.timeoutInterval = 10
|
}
|
||||||
return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
|
return decode(data, inResponseTo: message.message, verifyUsing: deviceKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func send(_ message: SignedMessage, toServerUrl server: String, authenticateWith authToken: Data, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
private func send(_ message: SignedMessage, toServerUrl server: String, authenticateWith authToken: Data, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||||
@ -71,6 +68,10 @@ final class Client {
|
|||||||
guard response == .messageAccepted, let data = responseData else {
|
guard response == .messageAccepted, let data = responseData else {
|
||||||
return message.with(result: response)
|
return message.with(result: response)
|
||||||
}
|
}
|
||||||
|
return decode(data, inResponseTo: message, verifyUsing: deviceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decode(_ data: Data, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) -> Message {
|
||||||
guard data.count == SignedMessage.size else {
|
guard data.count == SignedMessage.size else {
|
||||||
print("[WARN] Received message with \(data.count) bytes (\(Array(data)))")
|
print("[WARN] Received message with \(data.count) bytes (\(Array(data)))")
|
||||||
return message.with(result: .invalidMessageSizeFromDevice)
|
return message.with(result: .invalidMessageSizeFromDevice)
|
||||||
|
@ -25,6 +25,9 @@ final class RequestCoordinator: ObservableObject {
|
|||||||
@AppStorage("localIP")
|
@AppStorage("localIP")
|
||||||
var localAddress: String = "192.168.178.104/"
|
var localAddress: String = "192.168.178.104/"
|
||||||
|
|
||||||
|
@AppStorage("localPort")
|
||||||
|
var localPort: UInt16 = 8888
|
||||||
|
|
||||||
@AppStorage("connectionType")
|
@AppStorage("connectionType")
|
||||||
var connectionType: ConnectionStrategy = .remoteFirst
|
var connectionType: ConnectionStrategy = .remoteFirst
|
||||||
|
|
||||||
@ -171,7 +174,7 @@ final class RequestCoordinator: ObservableObject {
|
|||||||
return (message.with(result: .noKeyAvailable), nil)
|
return (message.with(result: .noKeyAvailable), nil)
|
||||||
}
|
}
|
||||||
let url = url(for: route)
|
let url = url(for: route)
|
||||||
return await client.send(message, to: url, through: route, using: keys)
|
return await client.send(message, to: url, port: localPort, through: route, using: keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetState() {
|
func resetState() {
|
||||||
@ -196,3 +199,17 @@ final class RequestCoordinator: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension UInt16: RawRepresentable {
|
||||||
|
|
||||||
|
public var rawValue: String {
|
||||||
|
"\(self)"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(rawValue: String) {
|
||||||
|
guard let value = UInt16(rawValue) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
170
Sesame/Common/UDPClient.swift
Normal file
170
Sesame/Common/UDPClient.swift
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
|
enum UDPState: String {
|
||||||
|
case initial
|
||||||
|
case connectionCreated
|
||||||
|
case preparingConnection
|
||||||
|
case sending
|
||||||
|
case waitingForResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UDPClient {
|
||||||
|
|
||||||
|
let host: NWEndpoint.Host
|
||||||
|
|
||||||
|
let port: NWEndpoint.Port
|
||||||
|
|
||||||
|
private var connection: NWConnection?
|
||||||
|
|
||||||
|
private var completion: ((Data?) -> Void)?
|
||||||
|
|
||||||
|
private var state: UDPState = .initial
|
||||||
|
|
||||||
|
init(host: String, port: UInt16) {
|
||||||
|
self.host = .init("192.168.188.118")
|
||||||
|
self.port = .init(rawValue: port)!
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
print("Destroying UDP Client")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func begin() {
|
||||||
|
guard state == .initial else {
|
||||||
|
print("Invalid state for begin(): \(state)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connection = NWConnection(host: host, port: port, using: .udp)
|
||||||
|
state = .connectionCreated
|
||||||
|
print("Created connection: \(connection != nil)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(message: Data, completion: @escaping (Data?) -> Void) {
|
||||||
|
print("Sending message to \(host) at port \(port)")
|
||||||
|
guard state == .connectionCreated else {
|
||||||
|
print("Invalid state preparing for send: \(state)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let connection else {
|
||||||
|
print("Failed to send, no connection")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.completion = completion
|
||||||
|
connection.stateUpdateHandler = { [weak self] (newState) in
|
||||||
|
switch (newState) {
|
||||||
|
case .ready:
|
||||||
|
print("State: Ready\n")
|
||||||
|
self?.send(message, over: connection)
|
||||||
|
case .setup:
|
||||||
|
print("State: Setup\n")
|
||||||
|
case .cancelled:
|
||||||
|
print("Cancelled UDP connection")
|
||||||
|
self?.finish()
|
||||||
|
case .preparing:
|
||||||
|
print("Preparing UDP connection")
|
||||||
|
case .failed(let error):
|
||||||
|
print("Failed to start UDP connection: \(error)")
|
||||||
|
self?.finish()
|
||||||
|
// default:
|
||||||
|
// print("ERROR! State not defined!\n")
|
||||||
|
// self?.finish()
|
||||||
|
case .waiting(_):
|
||||||
|
print("Waiting for UDP connection path change")
|
||||||
|
@unknown default:
|
||||||
|
print("Unknown UDP connection state: \(newState)")
|
||||||
|
self?.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print("Preparing connection")
|
||||||
|
state = .preparingConnection
|
||||||
|
connection.start(queue: .global())
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||||
|
guard self.state == .preparingConnection else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Timed out preparing connection")
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finish(_ data: Data? = nil) {
|
||||||
|
completion?(data)
|
||||||
|
completion = nil
|
||||||
|
connection?.stateUpdateHandler = nil
|
||||||
|
connection?.cancel()
|
||||||
|
connection = nil
|
||||||
|
state = .initial
|
||||||
|
}
|
||||||
|
|
||||||
|
private func send(_ data: Data, over connection: NWConnection) {
|
||||||
|
guard state == .preparingConnection else {
|
||||||
|
print("Invalid state for send: \(state)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.stateUpdateHandler = nil
|
||||||
|
|
||||||
|
let completion = NWConnection.SendCompletion.contentProcessed { [weak self] error in
|
||||||
|
if let error {
|
||||||
|
print("Failed to send UDP packet: \(error)")
|
||||||
|
self?.finish()
|
||||||
|
} else {
|
||||||
|
print("Finished sending message")
|
||||||
|
self?.waitForResponse(over: connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = .sending
|
||||||
|
connection.send(content: data, completion: completion)
|
||||||
|
print("Started to send message")
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||||
|
guard self.state == .sending else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Timed out waiting for for send to complete")
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func waitForResponse(over connection: NWConnection) {
|
||||||
|
guard state == .sending else {
|
||||||
|
print("Invalid state for send: \(state)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = .waitingForResponse
|
||||||
|
connection.receiveMessage { [weak self] (data, context, isComplete, error) in
|
||||||
|
guard self?.state == .waitingForResponse else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard isComplete else {
|
||||||
|
if let error {
|
||||||
|
print("Failed to receive UDP message: \(error)")
|
||||||
|
} else {
|
||||||
|
print("Failed to receive complete UDP message without error")
|
||||||
|
}
|
||||||
|
self?.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let data else {
|
||||||
|
print("Received UDP message without data")
|
||||||
|
self?.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Received \(data.count) bytes")
|
||||||
|
self?.finish(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||||
|
guard self.state == .waitingForResponse else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Timed out waiting for response")
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,10 @@ import SwiftData
|
|||||||
|
|
||||||
struct HistoryView: View {
|
struct HistoryView: View {
|
||||||
|
|
||||||
@Query
|
@Environment(\.modelContext)
|
||||||
|
private var context
|
||||||
|
|
||||||
|
@Query(sort: \HistoryItem.startDate, order: .reverse)
|
||||||
private var items: [HistoryItem] = []
|
private var items: [HistoryItem] = []
|
||||||
|
|
||||||
private var unlockCount: Int {
|
private var unlockCount: Int {
|
||||||
@ -38,7 +41,17 @@ struct HistoryView: View {
|
|||||||
}
|
}
|
||||||
ForEach(items) {entry in
|
ForEach(items) {entry in
|
||||||
HistoryListItem(entry: entry)
|
HistoryListItem(entry: entry)
|
||||||
|
}.onDelete(perform: { indexSet in
|
||||||
|
let objects = indexSet.map { items[$0] }
|
||||||
|
for object in objects {
|
||||||
|
context.delete(object)
|
||||||
}
|
}
|
||||||
|
do {
|
||||||
|
try context.save()
|
||||||
|
} catch {
|
||||||
|
print("Failed to save after deleting \(objects.count) object(s): \(error)")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.navigationTitle("History")
|
.navigationTitle("History")
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,8 @@ struct ContentView: View {
|
|||||||
keyManager: coordinator.keyManager,
|
keyManager: coordinator.keyManager,
|
||||||
coordinator: coordinator,
|
coordinator: coordinator,
|
||||||
serverAddress: $coordinator.serverPath,
|
serverAddress: $coordinator.serverPath,
|
||||||
localAddress: $coordinator.localAddress)
|
localAddress: $coordinator.localAddress,
|
||||||
|
localPort: $coordinator.localPort)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showHistorySheet) { HistoryView() }
|
.sheet(isPresented: $showHistorySheet) { HistoryView() }
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
import SwiftData
|
||||||
import SFSafeSymbols
|
import SFSafeSymbols
|
||||||
|
import Combine
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
|
||||||
@ -15,6 +16,12 @@ struct SettingsView: View {
|
|||||||
@Binding
|
@Binding
|
||||||
var localAddress: String
|
var localAddress: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var localPort: UInt16
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var localPortString = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@ -45,6 +52,19 @@ struct SettingsView: View {
|
|||||||
TextField("Local address", text: $localAddress)
|
TextField("Local address", text: $localAddress)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(.leading, 8)
|
.padding(.leading, 8)
|
||||||
|
TextField("UDP Port", text: $localPortString)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
.onReceive(Just(localPortString)) { newValue in
|
||||||
|
let filtered = newValue.filter { "0123456789".contains($0) }
|
||||||
|
if filtered != newValue {
|
||||||
|
self.localPortString = filtered
|
||||||
|
if let value = UInt16(filtered) {
|
||||||
|
self.localPort = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.leading, 8)
|
||||||
}.padding(.vertical, 8)
|
}.padding(.vertical, 8)
|
||||||
ForEach(KeyManagement.KeyType.allCases) { keyType in
|
ForEach(KeyManagement.KeyType.allCases) { keyType in
|
||||||
SingleKeyView(
|
SingleKeyView(
|
||||||
@ -52,10 +72,8 @@ struct SettingsView: View {
|
|||||||
type: keyType)
|
type: keyType)
|
||||||
}
|
}
|
||||||
}.padding()
|
}.padding()
|
||||||
}.onDisappear {
|
}.onAppear {
|
||||||
if !localAddress.hasSuffix("/") {
|
self.localPortString = "\(localPort)"
|
||||||
localAddress += "/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
}
|
}
|
||||||
@ -74,7 +92,8 @@ struct SettingsView: View {
|
|||||||
keyManager: KeyManagement(),
|
keyManager: KeyManagement(),
|
||||||
coordinator: .init(modelContext: container.mainContext),
|
coordinator: .init(modelContext: container.mainContext),
|
||||||
serverAddress: .constant("https://example.com"),
|
serverAddress: .constant("https://example.com"),
|
||||||
localAddress: .constant("192.168.178.42"))
|
localAddress: .constant("192.168.178.42"),
|
||||||
|
localPort: .constant(1234))
|
||||||
} catch {
|
} catch {
|
||||||
fatalError("Failed to create model container.")
|
fatalError("Failed to create model container.")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user