aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline/Hotline/HotlineClient.swift61
-rw-r--r--Hotline/Hotline/HotlineProtocol.swift46
-rw-r--r--Hotline/Hotline/HotlineTrackerClient.swift21
-rw-r--r--Hotline/Utility/FoundationExtensions.swift12
-rw-r--r--Hotline/Views/FilesView.swift141
-rw-r--r--Hotline/Views/TrackerView.swift4
6 files changed, 169 insertions, 116 deletions
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift
index c325fb7..59a1f9d 100644
--- a/Hotline/Hotline/HotlineClient.swift
+++ b/Hotline/Hotline/HotlineClient.swift
@@ -9,36 +9,6 @@ enum HotlineClientStatus: Int {
case loggedIn
}
-enum HotlineChatType: Int {
- case message
- case agreement
- case status
-}
-
-struct HotlineChat: Identifiable {
- let id = UUID()
- let text: String
- let username: String
- let type: HotlineChatType
-
- static let parser = /^\s*([^\:]+)\:\s*(.*)/
-
- init(text: String, type: HotlineChatType = .message) {
- self.type = type
-
- if
- type == .message,
- let match = text.firstMatch(of: HotlineChat.parser) {
- self.username = String(match.1)
- self.text = String(match.2)
- }
- else {
- self.username = ""
- self.text = text
- }
- }
-}
-
@Observable
class HotlineClient {
// static let shared = HotlineClient()
@@ -64,7 +34,7 @@ class HotlineClient {
var server: HotlineServer?
@ObservationIgnored private var connection: NWConnection?
- @ObservationIgnored private var transactionLog: [UInt32:HotlineTransactionType] = [:]
+ @ObservationIgnored private var transactionLog: [UInt32:(HotlineTransactionType, (() -> Void)?)] = [:]
init() {
// let downloadsPath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)
@@ -136,14 +106,14 @@ class HotlineClient {
// MARK: -
- private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil) {
+ private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil, reply: (() -> Void)? = nil) {
guard let c = connection else {
return
}
print("HotlineClient => \(t.id) \(t.type)")
- self.transactionLog[t.id] = t.type
+ self.transactionLog[t.id] = (t.type, reply)
c.send(content: t.encoded(), completion: .contentProcessed { [weak self] (error) in
if disconnectOnError, error != nil {
@@ -155,6 +125,10 @@ class HotlineClient {
})
}
+ private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil) {
+ sendTransaction(t, autodisconnect: disconnectOnError, callback: callback, reply: nil)
+ }
+
private func receiveTransaction() {
guard let c = connection else {
print("HotlineClient: no connection for transaction.")
@@ -379,12 +353,16 @@ class HotlineClient {
self.sendTransaction(t, callback: callback)
}
- func sendGetFileList(path: String? = nil, callback: (() -> Void)? = nil) {
- let t = HotlineTransaction(type: .getFileNameList)
+ func sendGetFileList(path: [String] = [], callback: (() -> Void)? = nil, reply: (() -> Void)?) {
+ var t = HotlineTransaction(type: .getFileNameList)
+
+ if !path.isEmpty {
+ t.setFieldPath(type: .filePath, val: path)
+ }
// if let p = path {
// t.setFieldString(type: .filePath)
// }
- self.sendTransaction(t, callback: callback)
+ self.sendTransaction(t, callback: callback, reply: reply)
}
// func sendGetNews(callback: (() -> Void)? = nil) {
@@ -409,11 +387,18 @@ class HotlineClient {
return
}
+ defer {
+ let replyCallback = repliedTransactionType.1
+ DispatchQueue.main.async {
+ replyCallback?()
+ }
+ }
+
self.transactionLog[transaction.id] = nil
print("HotlineClient reply in response to \(repliedTransactionType)")
- switch(repliedTransactionType) {
+ switch(repliedTransactionType.0) {
case .login:
print("GOT REPLY TO LOGIN!")
@@ -483,6 +468,8 @@ class HotlineClient {
default:
break
}
+
+
}
private func processTransaction(_ transaction: HotlineTransaction) {
diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift
index a0e129e..92d7449 100644
--- a/Hotline/Hotline/HotlineProtocol.swift
+++ b/Hotline/Hotline/HotlineProtocol.swift
@@ -17,15 +17,33 @@ struct HotlineServer: Identifiable, Hashable {
}
}
-extension UInt32 {
- func decodeFromUInt32() -> String {
- let bytes = [
- UInt8((self >> 24) & 0xFF),
- UInt8((self >> 16) & 0xFF),
- UInt8((self >> 8) & 0xFF),
- UInt8(self & 0xFF)
- ]
- return String(bytes: bytes, encoding: .utf8) ?? ""
+enum HotlineChatType: Int {
+ case message
+ case agreement
+ case status
+}
+
+struct HotlineChat: Identifiable {
+ let id = UUID()
+ let text: String
+ let username: String
+ let type: HotlineChatType
+
+ static let parser = /^\s*([^\:]+)\:\s*(.*)/
+
+ init(text: String, type: HotlineChatType = .message) {
+ self.type = type
+
+ if
+ type == .message,
+ let match = text.firstMatch(of: HotlineChat.parser) {
+ self.username = String(match.1)
+ self.text = String(match.2)
+ }
+ else {
+ self.username = ""
+ self.text = text
+ }
}
}
@@ -109,8 +127,8 @@ struct HotlineFile: Identifiable, Hashable {
let typeRaw = data.readUInt32(at: 0)!
let creatorRaw = data.readUInt32(at: 4)!
- self.type = typeRaw.decodeFromUInt32()
- self.creator = creatorRaw.decodeFromUInt32()
+ self.type = typeRaw.toStringLiteral()
+ self.creator = creatorRaw.toStringLiteral()
self.fileSize = data.readUInt32(at: 8)!
self.isFolder = (self.type == "fldr")
@@ -435,6 +453,7 @@ enum HotlineTransactionFieldType: UInt16 {
case communityBannerID = 161 // Integer
case serverName = 162 // String
case fileNameWithInfo = 200 // Data { type: 4, creator: 4, file size: 4, reserved: 4, name script: 2, name size: 2, name data: size }
+ case filePath = 202 // Path
// TODO: Add file field types
case quotingMessage = 214 // String?
case automaticResponse = 215 // String
@@ -460,6 +479,11 @@ enum HotlineTransactionFieldType: UInt16 {
case newsArticleRecursiveDelete = 337 // Integer
}
+func transactionTypeHasReply(_ type: HotlineTransactionType) -> Bool {
+
+ return false
+}
+
enum HotlineTransactionType: UInt16 {
case reply = 0
case error = 100
diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift
index 78f681f..4a24520 100644
--- a/Hotline/Hotline/HotlineTrackerClient.swift
+++ b/Hotline/Hotline/HotlineTrackerClient.swift
@@ -255,31 +255,10 @@ class HotlineTrackerClient {
if let name = serverName,
let desc = serverDescription {
let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
-
- print("SERVER: \(server)")
-
foundServers.append(server)
-
-
}
cursor += 10 + nameByteCount + descByteCount
-
-// let nameLength = Int(nameLengthByte)
-// if let name = self.bytes.readString(at: cursor + 11, length: nameLength) {
-// if let descLengthByte = self.bytes.readUInt8(at: cursor + 11 + nameLength) {
-// let descLength = Int(descLengthByte)
-// if let desc = self.bytes.readString(at: cursor + 11 + nameLength + 1, length: descLength) {
-// let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
-//
-// print("SERVER: \(server)")
-//
-// foundServers.append(server)
-//
-// cursor += 11 + nameLength + 1 + descLength
-// }
-// }
-// }
}
print(cursor)
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index c6ca251..000314e 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -94,3 +94,15 @@ extension Data {
// append(&val, count: MemoryLayout<UInt32>.size)
}
}
+
+extension UInt32 {
+ func toStringLiteral() -> String {
+ let bytes = [
+ UInt8((self >> 24) & 0xFF),
+ UInt8((self >> 16) & 0xFF),
+ UInt8((self >> 8) & 0xFF),
+ UInt8(self & 0xFF)
+ ]
+ return String(bytes: bytes, encoding: .utf8) ?? ""
+ }
+}
diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift
index 1395ca6..52963e0 100644
--- a/Hotline/Views/FilesView.swift
+++ b/Hotline/Views/FilesView.swift
@@ -1,22 +1,25 @@
import SwiftUI
import UniformTypeIdentifiers
-struct FilesView: View {
+struct FileListView: View {
@Environment(HotlineClient.self) private var hotline
@State private var fetched = false
+ @State var fileList: [HotlineFile] = []
+
+ var path: [String] = []
static let byteFormatter = ByteCountFormatter()
private func formattedFileSize(_ fileSize: UInt32) -> String {
-// let bcf = ByteCountFormatter()
- FilesView.byteFormatter.allowedUnits = [.useAll]
- FilesView.byteFormatter.countStyle = .file
- return FilesView.byteFormatter.string(fromByteCount: Int64(fileSize))
+ // let bcf = ByteCountFormatter()
+ FileListView.byteFormatter.allowedUnits = [.useAll]
+ FileListView.byteFormatter.countStyle = .file
+ return FileListView.byteFormatter.string(fromByteCount: Int64(fileSize))
}
private func fileIcon(name: String) -> UIImage {
-// func utTypeForFilename(_ filename: String) -> UTType? {
+ // func utTypeForFilename(_ filename: String) -> UTType? {
let fileExtension = (name as NSString).pathExtension
if let fileType = UTType(filenameExtension: fileExtension) {
print("\(name) \(fileExtension) = \(fileType)")
@@ -42,57 +45,103 @@ struct FilesView: View {
}
var body: some View {
- NavigationStack {
- List(hotline.fileList, id: \.self, children: \.files) { tree in
- if tree.isFolder {
- DisclosureGroup {
- HStack {
- HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
- Image(systemName: "folder.fill")
- }
- .frame(minWidth: 25)
- Text("File").fontWeight(.medium)
- Spacer()
- Text("4").foregroundStyle(.secondary)
-// HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
-// }
-// .frame(minWidth: 25)
-// ProgressView(value: 0.8)
- }
- } label: {
- HStack {
- HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
- Image(systemName: "folder.fill")
- }
- .frame(minWidth: 25)
- Text(tree.name).fontWeight(.medium)
- Spacer()
- Text("\(tree.fileSize)").foregroundStyle(.secondary)
- }
+ List {
+ ForEach(fileList, id: \.self) { file in
+ if file.isFolder {
+ DisclosureGroup {
+ if !fetched {
+ ProgressView()
}
- }
- else {
+ else {
+ FileListView(path: [])
+ }
+ } label: {
HStack {
HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
- Image(uiImage: fileIcon(name: tree.name))
- .renderingMode(.template)
- .foregroundColor(.accentColor)
+ Image(systemName: "folder.fill")
}
.frame(minWidth: 25)
- Text(tree.name)
+ Text(file.name).fontWeight(.medium)
Spacer()
- Text(formattedFileSize(tree.fileSize)).foregroundStyle(.gray)
+ Text("\(file.fileSize)").foregroundStyle(.secondary)
}
}
- }
- .listStyle(.plain)
- .task {
- if !fetched {
- hotline.sendGetFileList() {
- fetched = true
+ }
+ else {
+ HStack {
+ HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
+ Image(uiImage: fileIcon(name: file.name))
+ .renderingMode(.template)
+ .foregroundColor(.accentColor)
+ }
+ .frame(minWidth: 25)
+ Text(file.name)
+ Spacer()
+ Text(formattedFileSize(file.fileSize)).foregroundStyle(.gray)
}
}
}
+ }
+ .listStyle(.plain)
+ .task {
+ if !fetched {
+ hotline.sendGetFileList() {
+ print("FETCHED!")
+ fetched = true
+ } reply: {
+ print("GOT FILES REPLY?")
+ fileList = hotline.fileList
+ }
+ }
+ }
+ }
+}
+
+struct FilesView: View {
+ @Environment(HotlineClient.self) private var hotline
+
+ @State private var fetched = false
+
+// let fileList: [HotlineFile]
+
+// static let byteFormatter = ByteCountFormatter()
+//
+// private func formattedFileSize(_ fileSize: UInt32) -> String {
+// // let bcf = ByteCountFormatter()
+// FilesView.byteFormatter.allowedUnits = [.useAll]
+// FilesView.byteFormatter.countStyle = .file
+// return FilesView.byteFormatter.string(fromByteCount: Int64(fileSize))
+// }
+//
+// private func fileIcon(name: String) -> UIImage {
+// // func utTypeForFilename(_ filename: String) -> UTType? {
+// let fileExtension = (name as NSString).pathExtension
+// if let fileType = UTType(filenameExtension: fileExtension) {
+// print("\(name) \(fileExtension) = \(fileType)")
+//
+// if fileType.isSubtype(of: .movie) {
+// return UIImage(systemName: "play.rectangle")!
+// }
+// else if fileType.isSubtype(of: .image) {
+// return UIImage(systemName: "photo")!
+// }
+// else if fileType.isSubtype(of: .archive) {
+// return UIImage(systemName: "doc.zipper")!
+// }
+// else if fileType.isSubtype(of: .text) {
+// return UIImage(systemName: "doc.text")!
+// }
+// else {
+// return UIImage(systemName: "doc")!
+// }
+// }
+//
+// return UIImage(systemName: "doc")!
+// }
+
+ var body: some View {
+ NavigationStack {
+ FileListView(path: [])
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift
index 7f59118..c8d0f2e 100644
--- a/Hotline/Views/TrackerView.swift
+++ b/Hotline/Views/TrackerView.swift
@@ -92,7 +92,9 @@ struct TrackerView: View {
Text("\(server.address)").opacity(0.3).font(.system(size: 13))
}
Spacer()
- Text("\(server.users)").opacity(0.3).font(.system(size: 16)).fontWeight(.medium)
+ if server.users > 0 {
+ Text("\(server.users)").opacity(0.3).font(.system(size: 16)).fontWeight(.medium)
+ }
}
if server == selectedServer {
Spacer(minLength: 16)