diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-11-30 23:46:57 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-11-30 23:46:57 -0800 |
| commit | 885119acdd27bd53ed3096285c3eca3e2d99ad70 (patch) | |
| tree | 7dce98c071859684ff2aaa157f090b7cdc941a04 | |
| parent | 30255476781789df318a571622ab732681696139 (diff) | |
Bunch of work to and now login, user list, handling replies, etc. works. Or at least the framework is there for them to work.
| -rw-r--r-- | Hotline/Network/HotlineClient.swift | 179 | ||||
| -rw-r--r-- | Hotline/Network/HotlineProtocol.swift | 234 | ||||
| -rw-r--r-- | Hotline/TrackerView.swift | 150 | ||||
| -rw-r--r-- | Hotline/Utility/DataExtensions.swift | 20 |
4 files changed, 382 insertions, 201 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Network/HotlineClient.swift index 5193fe5..b5092a0 100644 --- a/Hotline/Network/HotlineClient.swift +++ b/Hotline/Network/HotlineClient.swift @@ -24,12 +24,14 @@ class HotlineClient : ObservableObject { @Published var userList: [HotlineUser] = [] @Published var chatMessages: [String] = [] - let userName: String = "bolt" - let userIconID: UInt32 = 128 - + var userName: String = "bolt" + var userIconID: UInt16 = 128 + var serverVersion: UInt16 = 151 var server: HotlineServer? var connection: NWConnection? + private var transactionLog: [UInt32:HotlineTransactionType] = [:] + init() { } @@ -94,6 +96,10 @@ class HotlineClient : ObservableObject { return } + print("HotlineClient => \(t.id) \(t.type)") + + self.transactionLog[t.id] = t.type + c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in if disconnectOnError, error != nil { self?.disconnect() @@ -110,7 +116,7 @@ class HotlineClient : ObservableObject { return } - print("HotlineClient: waiting for transaction...") + print("HotlineClient ⏳") c.receive(minimumIncompleteLength: HotlineTransaction.headerSize, maximumLength: HotlineTransaction.headerSize) { [weak self] (headerData, context, isComplete, error) in guard let self = self else { return @@ -127,39 +133,40 @@ class HotlineClient : ObservableObject { return } - print("HotlineClient: received \(headerData.count) header bytes") +// print("HotlineClient: received \(headerData.count) header bytes") if var transaction = self.parseTransaction(data: headerData) { // Receive additional data if the transaction has data attached to it. print("DATA SIZE: \(transaction.dataSize)") if transaction.dataSize > 0 { - c.receive(minimumIncompleteLength: Int(transaction.dataSize), maximumLength: Int(transaction.dataSize)) { [weak self] (parameterData, context, isComplete, error) in + c.receive(minimumIncompleteLength: Int(transaction.dataSize), maximumLength: Int(transaction.dataSize)) { [weak self] (fieldData, context, isComplete, error) in guard let self = self else { return } - guard let parameterData = parameterData, !parameterData.isEmpty else { - print("HotlineClient: transaction parameter data is empty!") + guard let fieldData = fieldData, !fieldData.isEmpty else { + print("HotlineClient: transaction field data is empty!") self.disconnect() return } - let parameterCount = parameterData.readUInt16(at: 0)! + let fieldCount = fieldData.readUInt16(at: 0)! - if parameterCount > 0 { + if fieldCount > 0 { var dataCursor = 2 - for _ in 0..<parameterCount { + for _ in 0..<fieldCount { if - let fieldID = parameterData.readUInt16(at: dataCursor), - let fieldSize = parameterData.readUInt16(at: dataCursor + 2), - let fieldData = parameterData.readData(at: dataCursor + 4, length: Int(fieldSize)) { + let fieldID = fieldData.readUInt16(at: dataCursor), + let fieldSize = fieldData.readUInt16(at: dataCursor + 2), + let fieldRemainingData = fieldData.readData(at: dataCursor + 4, length: Int(fieldSize)) { if let fieldType = HotlineTransactionFieldType(rawValue: fieldID) { - transaction.parameters.append(HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData)) -// transaction.parameters[fieldType] = HotlineTransactionParameter(type: fieldType, dataSize: fieldSize, data: fieldData) + + transaction.fields.append(HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldRemainingData)) +// transaction.parameters[fieldType] = HotlineTransactionField(type: fieldType, dataSize: fieldSize, data: fieldData) } else { - print("HotlineClient: UNKNOWN PARAM TYPE!", fieldID, fieldSize) + print("HotlineClient: UNKNOWN FIELD TYPE!", fieldID, fieldSize) } dataCursor += 4 + Int(fieldSize) @@ -201,10 +208,12 @@ class HotlineClient : ObservableObject { let transactionSize = data.readUInt32(at: 12), let dataSize = data.readUInt32(at: 16) { - print("HotlineClient: Parsing transaction type \(type) with data \(dataSize)") if let transactionType = HotlineTransactionType(rawValue: type) { return HotlineTransaction(type: transactionType, flags: flags, isReply: isReply, id: id, errorCode: errorCode, totalSize: transactionSize, dataSize: dataSize) } + else { + print("HotlineClient: Unknown type \(type) parsing with \(dataSize) bytes") + } } return nil @@ -249,8 +258,11 @@ class HotlineClient : ObservableObject { return } - print("HotlineClient: completed handshake") - self.sendLogin() + print("HotlineClient 🤝") + self.sendLogin() { [weak self] in + self?.sendSetClientUserInfo() + self?.sendGetUserList() + } self.receiveTransaction() } }) @@ -262,15 +274,13 @@ class HotlineClient : ObservableObject { } var t = HotlineTransaction(type: .login) - t.setParameterEncodedString(type: .userLogin, val: "") - t.setParameterEncodedString(type: .userPassword, val: "") - t.setParameterUInt32(type: .userIconID, val: self.userIconID) - t.setParameterString(type: .userName, val: self.userName) - t.setParameterUInt32(type: .versionNumber, val: 151) + t.setFieldEncodedString(type: .userLogin, val: "") + t.setFieldEncodedString(type: .userPassword, val: "") + t.setFieldUInt16(type: .userIconID, val: self.userIconID) + t.setFieldString(type: .userName, val: self.userName) + t.setFieldUInt32(type: .versionNumber, val: 123) - print("HotlineClient: logging in...") self.sendTransaction(t) { [weak self] in - print("HotlineClient: logged in!") DispatchQueue.main.async { self?.connectionStatus = .loggedIn } @@ -279,44 +289,100 @@ class HotlineClient : ObservableObject { } } + func sendSetClientUserInfo(callback: (() -> Void)? = nil) { + var t = HotlineTransaction(type: .setClientUserInfo) + t.setFieldString(type: .userName, val: self.userName) + t.setFieldUInt16(type: .userIconID, val: self.userIconID) + + self.sendTransaction(t, callback: callback) + } + func sendAgree(callback: (() -> Void)? = nil) { var t = HotlineTransaction(type: .agreed) - t.setParameterString(type: .userName, val: self.userName) - t.setParameterUInt32(type: .userIconID, val: self.userIconID) - t.setParameterUInt32(type: .options, val: 0) - - print("HotlineClient: agreeing") + t.setFieldString(type: .userName, val: self.userName) + t.setFieldUInt16(type: .userIconID, val: self.userIconID) + t.setFieldUInt32(type: .options, val: 0) self.sendTransaction(t, callback: callback) } func sendChat(message: String, callback: (() -> Void)? = nil) { var t = HotlineTransaction(type: .sendChat) - t.setParameterString(type: .data, val: message) - - print("HotlineClient: sending chat...") + t.setFieldString(type: .data, val: message) self.sendTransaction(t, callback: callback) } func sendGetUserList(callback: (() -> Void)? = nil) { let t = HotlineTransaction(type: .getUserNameList) - print("HotlineClient: fetching user list...") self.sendTransaction(t, callback: callback) } // MARK: - Incoming + private func processReply(_ transaction: HotlineTransaction) { + guard transaction.errorCode == 0 else { + if let errorParam = transaction.getField(type: .errorText), let errorText = errorParam.getString() { + print("HotlineClient 😵 \(transaction.errorCode): \(errorText)") + } + else { + print("HotlineClient 😵 \(transaction.errorCode)") + } + return + } + + guard let repliedTransactionType = self.transactionLog[transaction.id] else { + return + } + + print("HotlineClient reply in response to \(repliedTransactionType)") + + switch(repliedTransactionType) { + case .login: + print("GOT REPLY TO LOGIN!") + + if + let serverVersionField = transaction.getField(type: .versionNumber), + let serverVersion = serverVersionField.getUInt16() { + self.serverVersion = serverVersion + print("SERVER VERSION: \(serverVersion)") + } + case .getUserNameList: + print("GOT USER LIST") + var newUserList: [HotlineUser] = [] + for u in transaction.getFieldList(type: .userNameWithInfo) { + let info = u.getUserInfo() + print("USER: \(info)") + let user = HotlineUser(id: info.id, iconID: info.iconID, status: info.flags, name: info.userName) + newUserList.append(user) + } + DispatchQueue.main.async { + self.userList = newUserList + } + default: + break + } + } + private func processTransaction(_ transaction: HotlineTransaction) { + if transaction.type == .reply { + print("HotlineClient <= \(transaction.type) to \(transaction.id):") + print(transaction) + } + else { + print("HotlineClient <= \(transaction.type)") + } + switch(transaction.type) { case .reply: - print("HotlineClient: GOT REPLY TRANSACTION? \(transaction)") + self.processReply(transaction) +// print("HotlineClient: Received reply transaction: \(transaction)") + case .chatMessage: - print("HotlineClient: CHAT MESSAGE!") - if - let chatTextParam = transaction.getParameter(type: .data), + if + let chatTextParam = transaction.getField(type: .data), let chatText = chatTextParam.getString(), - let userNameParam = transaction.getParameter(type: .userName), + let userNameParam = transaction.getField(type: .userName), let userName = userNameParam.getString(), - let userIDParam = transaction.getParameter(type: .userID), + let userIDParam = transaction.getField(type: .userID), let userID = userIDParam.getUInt16() { print("HotlineClient: \(userName):\(userID): \(chatText)") DispatchQueue.main.async { @@ -325,37 +391,42 @@ class HotlineClient : ObservableObject { } case .getUserNameList: print("HotlineClient: GOT USER NAME LIST!") - let userList = transaction.getParameterList(type: .userInfo) + let userList = transaction.getFieldList(type: .userNameWithInfo) for u in userList { let userInfo = u.getUserInfo() print("HotlineClient: user \(userInfo.userName)") } case .notifyOfUserChange: - print("HotlineClient: user changed") - if let p = transaction.getParameter(type: .userName), +// print("HotlineClient: user changed") + if let p = transaction.getField(type: .userName), let userName = p.getString() { - print("HotlineClient: user name \(userName)") + print("HotlineClient: User changed \(userName)") } case .disconnectMessage: - print("HotlineClient: DISCONNECTED BY SERVER!") + print("HotlineClient ❌") self.disconnect() case .showAgreement: - if let agreementParam = transaction.getParameter(type: .data) { + if let noAgreementField = transaction.getField(type: .noServerAgreement) { + print("NO AGREEMENT?") + } + if let agreementParam = transaction.getField(type: .data) { if let agreementText = agreementParam.getString() { - print("AGREEMENT:", agreementText) + print("\n\n--------------------------\n") + print(agreementText) + print("\n--------------------------\n\n") DispatchQueue.main.async { self.agreement = agreementText } - self.sendAgree() { +// self.sendAgree() { // self.sendGetUserList() - } +// } } } case .userAccess: - print("HotlineClient: user access transaction.") + print("") default: - print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.parameters.count) parameters") - print(transaction.parameters) + print("HotlineClient: UNKNOWN transaction \(transaction.type) with \(transaction.fields.count) parameters") + print(transaction.fields) } } } diff --git a/Hotline/Network/HotlineProtocol.swift b/Hotline/Network/HotlineProtocol.swift index d5fe734..5372e78 100644 --- a/Hotline/Network/HotlineProtocol.swift +++ b/Hotline/Network/HotlineProtocol.swift @@ -19,15 +19,58 @@ struct HotlineServer: Identifiable, Hashable { struct HotlineUser: Identifiable, Hashable { let id: UInt16 - let userName: String + let iconID: UInt16 + let status: UInt16 + let name: String + + var isAdmin: Bool { + return ((self.status & 0x0002) != 0) + } + + var isIdle: Bool { + return ((self.status & 0x0001) != 0) + } static func == (lhs: HotlineUser, rhs: HotlineUser) -> Bool { return lhs.id == rhs.id } + init(id: UInt16, iconID: UInt16, status: UInt16, name: String) { + self.id = id + self.iconID = iconID + self.status = status + self.name = name + } + + init(from data: Data) { + self.id = data.readUInt16(at: 0)! + self.iconID = data.readUInt16(at: 2)! + self.status = data.readUInt16(at: 4)! + + let userNameLength = Int(data.readUInt16(at: 6)!) + self.name = data.readString(at: 8, length: userNameLength, encoding: .ascii)! + } + func hash(into hasher: inout Hasher) { hasher.combine(self.id) } + + func encoded() -> Data { + var data = Data() + self.encode(to: &data) + return data + } + + func encode(to data: inout Data) { + data.appendUInt16(self.id) + data.appendUInt16(self.iconID) + data.appendUInt16(self.status) + + let userNameData = name.data(using: .ascii, allowLossyConversion: true)! + + data.appendUInt16(UInt16(userNameData.count)) + data.append(userNameData) + } } struct HotlineUserInfo { @@ -46,21 +89,82 @@ struct HotlineUserInfo { } } -struct HotlineTransactionParameter { +struct HotlineTransactionField { let type: HotlineTransactionFieldType let dataSize: UInt16 let data: Data + init(type: HotlineTransactionFieldType, dataSize: UInt16, data: Data) { + self.type = type + self.dataSize = dataSize + self.data = data + } + + init(type: HotlineTransactionFieldType, val: UInt8) { + self.init(type: type, dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val)) + } + + init(type: HotlineTransactionFieldType, val: UInt16) { + self.init(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val)) + } + + init(type: HotlineTransactionFieldType, val: UInt32) { + self.init(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val)) + } + + init(type: HotlineTransactionFieldType, string: String, encoding: String.Encoding = .ascii, encrypt: Bool = false) { + var stringInput = string + + if encrypt { + stringInput = String(string.utf8.map { char in + Character(UnicodeScalar(0xFF - char)) + }) + } + + var stringData: Data? + stringData = stringInput.data(using: encoding, allowLossyConversion: true) + if stringData == nil { + stringData = Data() + } + + self.init(type: type, dataSize: UInt16(stringData!.count), data: stringData!) + } + + init(type: HotlineTransactionFieldType, string: String, encrypt: Bool) { + self.init(type: type, string: string, encoding: .ascii, encrypt: encrypt) + } + func getUInt8() -> UInt8? { - return data.readUInt8(at: 0) + return self.data.readUInt8(at: 0) } func getUInt16() -> UInt16? { - return data.readUInt16(at: 0) + return self.data.readUInt16(at: 0) } func getUInt32() -> UInt32? { - return data.readUInt32(at: 0) + return self.data.readUInt32(at: 0) + } + + func getInteger() -> Int? { + switch(self.data.count) { + case 1: + if let val = self.getUInt8() { + return Int(val) + } + case 2: + if let val = self.getUInt16() { + return Int(val) + } + case 4: + if let val = self.getUInt32() { + return Int(val) + } + default: + break + } + + return nil } func getString(encoding: String.Encoding = .ascii) -> String? { @@ -89,7 +193,7 @@ struct HotlineTransaction { var totalSize: UInt32 = UInt32(HotlineTransaction.headerSize) var dataSize: UInt32 = 0 - var parameters: [HotlineTransactionParameter] = [] + var fields: [HotlineTransactionField] = [] init(type: HotlineTransactionType) { self.type = type @@ -105,48 +209,34 @@ struct HotlineTransaction { self.dataSize = dataSize } - mutating func setParameterUInt8(type: HotlineTransactionFieldType, val: UInt8) { - self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val))) -// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(MemoryLayout<UInt8>.size), data: Data(val)) + mutating func setFieldUInt8(type: HotlineTransactionFieldType, val: UInt8) { + self.fields.append(HotlineTransactionField(type: type, val: val)) } - mutating func setParameterUInt16(type: HotlineTransactionFieldType, val: UInt16) { - self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val))) -// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(MemoryLayout<UInt16>.size), data: Data(val)) + mutating func setFieldUInt16(type: HotlineTransactionFieldType, val: UInt16) { + self.fields.append(HotlineTransactionField(type: type, val: val)) } - mutating func setParameterUInt32(type: HotlineTransactionFieldType, val: UInt32) { - self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val))) -// self.parameters[type] = HotlineTransactionParameter(type: type, dataSize: UInt16(MemoryLayout<UInt32>.size), data: Data(val)) + mutating func setFieldUInt32(type: HotlineTransactionFieldType, val: UInt32) { + self.fields.append(HotlineTransactionField(type: type, val: val)) } - mutating func setParameterEncodedString(type: HotlineTransactionFieldType, val: String) { - let encodedVal = String(val.utf8.map { char in - Character(UnicodeScalar(0xFF - char)) - }) - - self.setParameterString(type: type, val: encodedVal) + mutating func setFieldEncodedString(type: HotlineTransactionFieldType, val: String) { + self.fields.append(HotlineTransactionField(type: type, string: val, encrypt: true)) } - mutating func setParameterString(type: HotlineTransactionFieldType, val: String) { - var stringData = Data() -// stringData.appendUInt16(UInt16(val.count)) - stringData.append(contentsOf: val.utf8) - - self.parameters.append(HotlineTransactionParameter(type: type, dataSize: UInt16(stringData.count), data: stringData)) -// self.parameters[type] = HotlineTransactionParameter(dataSize: UInt16(stringData.count), data: stringData) + mutating func setFieldString(type: HotlineTransactionFieldType, val: String) { + self.fields.append(HotlineTransactionField(type: type, string: val)) } - func getParameter(type: HotlineTransactionFieldType) -> HotlineTransactionParameter? { - return self.parameters.first { p in + func getField(type: HotlineTransactionFieldType) -> HotlineTransactionField? { + return self.fields.first { p in p.type == type } - -// return self.parameters[type] } - func getParameterList(type: HotlineTransactionFieldType) -> [HotlineTransactionParameter] { - return self.parameters.filter { p in + func getFieldList(type: HotlineTransactionFieldType) -> [HotlineTransactionField] { + return self.fields.filter { p in p.type == type } } @@ -164,40 +254,76 @@ struct HotlineTransaction { data.appendUInt32(self.id) data.appendUInt32(self.errorCode) - if self.parameters.count > 0 { - var parameterData = Data() - parameterData.appendUInt16(UInt16(self.parameters.count)) - for param in self.parameters { - parameterData.appendUInt16(param.type.rawValue) - parameterData.appendUInt16(param.dataSize) - parameterData.append(param.data) + if self.fields.count > 0 { + var fieldData = Data() + fieldData.appendUInt16(UInt16(self.fields.count)) + for f in self.fields { + fieldData.appendUInt16(f.type.rawValue) + fieldData.appendUInt16(f.dataSize) + fieldData.append(f.data) } - data.appendUInt32(UInt32(parameterData.count)) - data.appendUInt32(UInt32(parameterData.count)) - data.append(parameterData) + data.appendUInt32(UInt32(fieldData.count)) + data.appendUInt32(UInt32(fieldData.count)) + data.append(fieldData) } else { - data.appendUInt32(0) - data.appendUInt32(0) + data.appendUInt32(2) + data.appendUInt32(2) + data.appendUInt16(0) } } } enum HotlineTransactionFieldType: UInt16 { + case errorText = 100 // String + case data = 101 // String case userName = 102 // String + case userID = 103 // Integer + case userIconID = 104 // Integer case userLogin = 105 // Encoded string case userPassword = 106 // Encoded string - case userIconID = 104 // Integer - case userID = 103 // Integer - case data = 101 // String - case userAccess = 110 // 64-bit integer?? - case userFlags = 112 + case referenceNumber = 107 // Integer + case transferSize = 108 // Integer + case chatOptions = 109 // Integer + case userAccess = 110 // 64-bit integer? + case userAlias = 111 // ??? + case userFlags = 112 // Integer case options = 113 // 32-bit integer? + case chatID = 114 // Integer + case chatSubject = 115 // String + case waitingCount = 116 // Integer + case serverAgreement = 150 // ??? + case serverBanner = 151 // Data? + case serverBannerType = 152 // Integer + case serverBannerURL = 153 // String + case noServerAgreement = 154 // Integer case versionNumber = 160 // Integer - case bannerID = 161 - case serverName = 162 - case userInfo = 300 + case communityBannerID = 161 // Integer + case serverName = 162 // String + // TODO: Add file field types + case quotingMessage = 214 // String? + case automaticResponse = 215 // String + case folderItemCount = 220 // Integer + case userNameWithInfo = 300 // Data { user id: 2, icon id: 2, user flags: 2, user name size: 2, user name: size } + case newsCategoryGUID = 319 // Data? + case newsCategoryListData = 320 // Data { type: 1 (1 = folder, 10 = category, 255 = other), category name: rest } + case newsArticleListData = 321 // Data + case newsCategoryName = 322 // String + case newsCategoryListData15 = 323 // Data + case newsPath = 325 // Data + case newsArticleID = 326 // Integer + case newsArticleDataFlavor = 327 // String + case newsArticleTitle = 328 // String + case newsArticlePoster = 329 // String + case newsArticleDate = 330 // Data { year: 2, ms: 2, secs: 4 } + case newsArticlePrevious = 331 // Integer + case newsArticleNext = 332 // Integer + case newsArticleData = 333 // Data + case newsArticleFlags = 334 // Integer + case newsArticleParentArticle = 335 // Integer + case newsArticleFirstChildArticle = 336 // Integer + case newsArticleRecursiveDelete = 337 // Integer } enum HotlineTransactionType: UInt16 { diff --git a/Hotline/TrackerView.swift b/Hotline/TrackerView.swift index 0bc9762..1b5cce5 100644 --- a/Hotline/TrackerView.swift +++ b/Hotline/TrackerView.swift @@ -1,112 +1,96 @@ import SwiftUI import SwiftData +struct AgreementView: View { + @Environment(\.dismiss) var dismiss + + let text: String + + var body: some View { + Text(text) + } +} + +struct TrackerServerView: View { + @Environment(\.dismiss) var dismiss + + let server: HotlineServer + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text("🌎") + Text(server.name!).bold().dynamicTypeSize(.xxLarge) + } + .padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)) + Text(server.description!).opacity(0.6).dynamicTypeSize(.xLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)) + Text(server.address).opacity(0.3).dynamicTypeSize(.medium) + Spacer() + HStack(alignment: .center) { + Button("Connect") { + print("WHAT", "HELLO", server.address) + HotlineClient.shared.connect(to: server) + dismiss() +// client = HotlineClient(server: selectedServer) + } + .bold() + .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) + .frame(maxWidth: .infinity) + .foregroundColor(.black) + .background(LinearGradient(gradient: Gradient(colors: [Color(white: 0.95), Color(white: 0.91)]), startPoint: .top, endPoint: .bottom)) + . + overlay( + RoundedRectangle(cornerRadius: 10.0).stroke(.black, lineWidth: 3).opacity(0.4) + ) + .cornerRadius(10.0) + } + } + .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0)) + .presentationDetents([.fraction(0.4)]) + .presentationDragIndicator(.automatic) + } +} + struct TrackerView: View { + // @Environment(\.modelContext) private var modelContext // @Query private var items: [Item] @StateObject var tracker = HotlineTracker(address: "hltracker.com") - @State private var selectedServer: HotlineServer? - - private var client: HotlineClient? + @StateObject var client = HotlineClient.shared + @State private var selectedServer: HotlineServer? + @State private var showingAgreement = false + @State private var showingConnectSheet = false + var body: some View { List(selection: $selectedServer) { ForEach(tracker.servers) { server in HStack { + Text("🌎") Text(server.name!).bold() Spacer() - HStack { - Text("\(server.users)").font(.system(size: 12)).bold().opacity(0.3) - // Image(systemName: "person.fill").font(.system(size: 12)).opacity(0.3) - } - .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10)) - .background(Color(white: 0.94)) - .cornerRadius(20.0) + Text("\(server.users)").opacity(0.6) } -// .contentShape(Rectangle()) - .listRowSeparator(.hidden) + .listRowBackground(Color(white: 0.96)) + .listRowInsets(EdgeInsets(top: 0, leading: 16.0, bottom: 0, trailing: 16.0)) + .listRowSeparator(.visible, edges: VerticalEdge.Set.all) + .listRowSeparatorTint(Color(white: 1.0)) .tag(server) - // .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 0)) -// .onTapGesture { -// self.selectedServer = server -// } } } + .background(Color(white: 0.96)) .listStyle(.plain) .frame(maxWidth: .infinity) .task { tracker.fetch() } .sheet(item: $selectedServer) { item in - VStack(alignment: .leading) { - Text(item.name!).bold().dynamicTypeSize(.xxLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)) - Text(item.description!).opacity(0.4).dynamicTypeSize(.xLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)) - Text(item.address).opacity(0.2).dynamicTypeSize(.medium) - Spacer() - HStack(alignment: .center) { - Button("Connect") { - print("WHAT", "HELLO", selectedServer!.address) - HotlineClient.shared.connect(to: selectedServer!) -// client = HotlineClient(server: selectedServer) - } - .bold() - .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .background(Color(.black)) - .cornerRadius(8.0) - } - } - .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0)) - .presentationDetents([.fraction(0.4)]) - .presentationDragIndicator(.visible) + TrackerServerView(server: item) + } + .sheet(isPresented: Binding(get: { client.agreement != nil }, set: { _ in })) { + AgreementView(text: client.agreement!) } -// List(tracker.servers) { server in -// HStack { -// Text(server.name!).bold() -// Spacer() -// HStack { -// Text("\(server.users)").font(.system(size: 12)).bold().opacity(0.3) -//// Image(systemName: "person.fill").font(.system(size: 12)).opacity(0.3) -// } -// .padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10)) -// .background(Color(white: 0.94)) -// .cornerRadius(20.0) -// } -// .contentShape(Rectangle()) -// .listRowSeparator(.hidden) -//// .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 0)) -// .onTapGesture { -// self.selectedServer = server -// } -// } -// .listStyle(.plain) -// .frame(maxWidth: .infinity) -// .task { -// tracker.fetch() -// } -// .sheet(item: $selectedServer) { item in -// VStack(alignment: .leading) { -// Text(item.name!).bold().dynamicTypeSize(.xxLarge).padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)) -// Text(item.description!).opacity(0.4).dynamicTypeSize(.xLarge) -// Spacer() -// HStack(alignment: .center) { -// Button("Connect") { -// print("WHAT") -// } -// .bold() -// .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) -// .frame(maxWidth: .infinity) -// .foregroundColor(.white) -// .background(Color(.black)) -// .cornerRadius(8.0) -// } -// } -// .padding(EdgeInsets(top: 28.0, leading: 24.0, bottom: 24.0, trailing: 24.0)) -// .presentationDetents([.fraction(0.3)]) -// .presentationDragIndicator(.visible) -// } } } diff --git a/Hotline/Utility/DataExtensions.swift b/Hotline/Utility/DataExtensions.swift index caf26a9..9f4c967 100644 --- a/Hotline/Utility/DataExtensions.swift +++ b/Hotline/Utility/DataExtensions.swift @@ -22,33 +22,33 @@ extension Data { } func readUInt8(at offset: Int) -> UInt8? { - guard offset >= 0, offset + MemoryLayout<UInt8>.size <= self.count else { + guard offset >= 0, offset + 1 <= self.count else { return nil } return self[offset] } - func readUInt16(at offset: Int, endianness: Endianness = .big) -> UInt16? { - guard offset >= 0, offset + MemoryLayout<UInt16>.size <= self.count else { + func readUInt16(at offset: Int) -> UInt16? { + guard offset >= 0, offset + 2 <= self.count else { return nil } - let value = self.subdata(in: offset..<(offset + MemoryLayout<UInt16>.size)).withUnsafeBytes { $0.load(as: UInt16.self) } - return (endianness == .big) ? value.bigEndian : value.littleEndian + + return (UInt16(self[offset]) << 8) + UInt16(self[offset + 1]) } - func readUInt32(at offset: Int, endianness: Endianness = .big) -> UInt32? { - guard offset >= 0, offset + MemoryLayout<UInt32>.size <= self.count else { + func readUInt32(at offset: Int) -> UInt32? { + guard offset >= 0, offset + 4 <= self.count else { return nil } - let value = self.subdata(in: offset..<(offset + MemoryLayout<UInt32>.size)).withUnsafeBytes { $0.load(as: UInt32.self) } - return (endianness == .big) ? value.bigEndian : value.littleEndian + + return (UInt32(self[offset]) << 24) + (UInt32(self[offset + 1]) << 16) + (UInt32(self[offset + 2]) << 8) + UInt32(self[offset + 3]) } func readData(at offset: Int, length: Int) -> Data? { guard offset >= 0, offset + length <= self.count else { return nil } - return self[offset..<(offset + length)] + return self.subdata(in: offset..<(offset + length)) } func readString(at offset: Int, length: Int, encoding: String.Encoding) -> String? { |