aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2023-12-08 16:12:47 -0800
committerDustin Mierau <dustin@mierau.me>2023-12-08 16:13:03 -0800
commit84aaa9ef0a1be6986e38d7e4f58e07d3cb84979e (patch)
treec0b3a36f14295a921413b7db0f1b37c9ec10e715 /Hotline
parent537ed932a4c8d639089f276e322de4e946b0d26b (diff)
Move tracker client to async/await. TrackerView uses Model now. Fix string encodings for MacRoman and japanese.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Hotline/HotlineProtocol.swift5
-rw-r--r--Hotline/Hotline/HotlineTrackerClient.swift134
-rw-r--r--Hotline/HotlineApp.swift4
-rw-r--r--Hotline/Models/Hotline.swift16
-rw-r--r--Hotline/Models/Server.swift16
-rw-r--r--Hotline/Models/Tracker.swift10
-rw-r--r--Hotline/Utility/FoundationExtensions.swift51
-rw-r--r--Hotline/Views/ChatView.swift25
-rw-r--r--Hotline/Views/FilesView.swift157
-rw-r--r--Hotline/Views/HotlineView.swift17
-rw-r--r--Hotline/Views/TrackerView.swift48
11 files changed, 187 insertions, 296 deletions
diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift
index e3fcd12..c126d03 100644
--- a/Hotline/Hotline/HotlineProtocol.swift
+++ b/Hotline/Hotline/HotlineProtocol.swift
@@ -143,6 +143,7 @@ class HotlineFile: Identifiable, Hashable {
// data.readUInt32(at: 12)! // reserved
// let nameScript = data.readUInt16(at: 16)! // name script
+// print("NAME SCRIPT: \(nameScript)")
let nameLength = data.readUInt16(at: 18)!
self.name = data.readString(at: 20, length: Int(nameLength))!
@@ -269,7 +270,7 @@ struct HotlineTransactionField {
for name in pathComponents {
pathData.appendUInt16(0)
- var nameData = name.data(using: .ascii, allowLossyConversion: true)
+ var nameData = name.data(using: .macOSRoman, allowLossyConversion: true)
if nameData == nil {
nameData = Data()
}
@@ -315,7 +316,7 @@ struct HotlineTransactionField {
}
func getString() -> String? {
- return String(data: self.data, encoding: .utf8) ?? String(data: self.data, encoding: .ascii)
+ return self.data.readString(at: 0, length: self.data.count)
}
func getUser() -> HotlineUser {
diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift
index f93949e..9bcf631 100644
--- a/Hotline/Hotline/HotlineTrackerClient.swift
+++ b/Hotline/Hotline/HotlineTrackerClient.swift
@@ -7,37 +7,37 @@ enum HotlineTrackerStatus: Int {
case connected
}
-struct HotlineTracker: Identifiable {
+struct HotlineTracker: Identifiable, Equatable {
+ let id: UUID = UUID()
var address: String
var port: UInt16
- var servers: [HotlineServer] = []
- var expanded: Bool = false
-
- var id: String { get { return self.address } }
-
+
init(_ address: String, port: UInt16 = 5498) {
self.address = address
self.port = port
}
+
+ static func == (lhs: HotlineTracker, rhs: HotlineTracker) -> Bool {
+ return lhs.address == rhs.address && lhs.port == rhs.port
+ }
}
-@Observable
class HotlineTrackerClient {
static let magicPacket = Data([
0x48, 0x54, 0x52, 0x4B, // 'HTRK'
0x00, 0x01 // Version
])
- @ObservationIgnored private var serverAddress: NWEndpoint.Host
- @ObservationIgnored private var serverPort: NWEndpoint.Port
- @ObservationIgnored private var connection: NWConnection?
- @ObservationIgnored private var bytes = Data()
- @ObservationIgnored private var maxDataLength: Int = 0
- @ObservationIgnored private var serverCount: Int = 0
+ private var tracker: HotlineTracker
+ private var connectionStatus: HotlineTrackerStatus = .disconnected
+ private var servers: [HotlineServer] = []
- var tracker: HotlineTracker
- var connectionStatus: HotlineTrackerStatus = .disconnected
- var servers: [HotlineServer] = []
+ private var serverAddress: NWEndpoint.Host
+ private var serverPort: NWEndpoint.Port
+ private var connection: NWConnection?
+ private var bytes = Data()
+ private var maxDataLength: Int = 0
+ private var serverCount: Int = 0
init() {
let t = HotlineTracker("hltracker.com")
@@ -51,29 +51,16 @@ class HotlineTrackerClient {
self.serverAddress = NWEndpoint.Host(tracker.address)
self.serverPort = NWEndpoint.Port(rawValue: tracker.port)!
}
-
- func fetch(callback: (() -> Void)? = nil) {
- self.reset()
- self.connect(callback)
- }
-
- func fetch2(address: String, port: Int, callback: (([Server]) -> Void)? = nil) {
+
+ func fetchServers(address: String, port: Int, callback: (([HotlineServer]) -> Void)? = nil) async -> [HotlineServer] {
self.serverAddress = NWEndpoint.Host(address)
self.serverPort = NWEndpoint.Port(rawValue: UInt16(port))!
self.reset()
- self.connect { [weak self] in
- var allServers: [Server] = []
-
- if let servers = self?.servers {
- for server in servers {
- let s = Server(name: server.name!, description: server.description, address: server.address, port: Int(server.port))
- allServers.append(s)
- }
- }
-
- DispatchQueue.main.async {
- callback?(allServers)
+
+ return await withCheckedContinuation { [weak self] continuation in
+ self?.connect { [weak self] in
+ continuation.resume(returning: self?.servers ?? [])
}
}
}
@@ -81,6 +68,7 @@ class HotlineTrackerClient {
private func reset() {
self.maxDataLength = 0
self.serverCount = 0
+ self.servers = []
}
private func connect(_ callback: (() -> Void)? = nil) {
@@ -88,37 +76,30 @@ class HotlineTrackerClient {
self.connection?.stateUpdateHandler = { [weak self] (newState: NWConnection.State) in
switch newState {
case .ready:
- print("READY TO SEND AND RECEIVE DATA")
- DispatchQueue.main.async {
- self?.connectionStatus = .connected
- }
+ self?.connectionStatus = .connected
self?.sendMagic()
case .cancelled:
- print("CONNECTION CANCELLED")
+ self?.connectionStatus = .disconnected
DispatchQueue.main.async {
- self?.connectionStatus = .disconnected
callback?()
}
case .failed(let err):
- print("CONNECTION ERROR \(err)")
+ print("HotlineTrackerClient: Connection error \(err)")
+ self?.connectionStatus = .disconnected
DispatchQueue.main.async {
- self?.connectionStatus = .disconnected
callback?()
}
default:
- print("CONNECTION OTHER THING")
+ return
}
}
- DispatchQueue.main.async {
- self.connectionStatus = .connecting
- }
+ self.connectionStatus = .connecting
self.connection?.start(queue: .global())
}
private func disconnect() {
guard let c = connection else {
- print("HotlineTracker: already disconnected")
return
}
@@ -128,7 +109,7 @@ class HotlineTrackerClient {
private func sendMagic() {
guard let c = connection else {
- print("HotlineTracker: invalid connection to send magic.")
+ print("HotlineTrackerClient: invalid connection to send magic.")
return
}
@@ -136,39 +117,33 @@ class HotlineTrackerClient {
c.send(content: HotlineTrackerClient.magicPacket, completion: .contentProcessed { [weak self] (error) in
if let err = error {
- print("HotlineTracker: sending magic failed \(err)")
+ print("HotlineTrackerClient: sending magic failed \(err)")
return
}
- print("HotlineTracker: sent magic!")
-
self?.receiveMagic()
})
}
private func receiveMagic() {
guard let c = connection else {
- print("HotlineTracker: invalid connection to receive magic.")
+ print("HotlineTrackerClient: invalid connection to receive magic.")
return
}
- print("HotlineTracker: receiving...")
c.receive(minimumIncompleteLength: 6, maximumLength: 6) { [weak self] (data, context, isComplete, error) in
guard let self = self, let data = data else {
return
}
if data.isEmpty || !data.elementsEqual(HotlineTrackerClient.magicPacket) {
- print("HotlineTracker: invalid magic response")
+ print("HotlineTrackerClient: invalid magic response")
self.disconnect()
return
}
- // if let data = data, !data.isEmpty {
- print("HotlineTracker: received magic response!")
- // }
if let error = error {
- print("HotlineTracker: receive error \(error)")
+ print("HotlineTrackerClient: receive error \(error)")
}
else {
self.receiveHeader()
@@ -178,35 +153,29 @@ class HotlineTrackerClient {
private func receiveHeader() {
guard let c = connection else {
- print("HotlineTracker: invalid connection to receive header.")
+ print("HotlineTrackerClient: invalid connection to receive header.")
return
}
- print("HotlineTracker: receiving...")
c.receive(minimumIncompleteLength: 8, maximumLength: 8) { [weak self] (data, context, isComplete, error) in
guard let self = self else {
return
}
if let error = error {
- print("HotlineTracker: receive error \(error)")
+ print("HotlineTrackerClient: receive error \(error)")
self.disconnect()
return
}
if let data = data, !data.isEmpty {
- print("HotlineTracker: received \(data.count) header bytes")
-
self.maxDataLength = Int(data[2]) * 0xFF + Int(data[3])
self.maxDataLength -= 4
- print("HotlineTracker: message size = \(self.maxDataLength)")
-
self.serverCount = Int(data[4]) * 256 + Int(data[5])
- print("HotlineTracker: server count = \(self.serverCount)")
}
if let error = error {
- print("HotlineTracker: receive error \(error)")
+ print("HotlineTrackerClient: receive error \(error)")
}
else {
self.receiveListing()
@@ -216,33 +185,32 @@ class HotlineTrackerClient {
private func receiveListing() {
guard let c = connection else {
- print("HotlineTracker: invalid connection to receive data.")
+ print("HotlineTrackerClient: invalid connection to receive data.")
return
}
- print("HotlineTracker: receiving...")
c.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { [weak self] (data, context, isComplete, error) in
guard let self = self else {
return
}
if let data = data, !data.isEmpty {
- print("HotlineTracker: received \(data.count) bytes")
self.bytes.append(contentsOf: data)
if bytes.count >= maxDataLength {
- self.disconnect()
self.parseListing()
+ print("HotlineTrackerClient: Found \(self.servers.count) servers on tracker \(self.serverAddress):\(self.serverPort)")
+ self.disconnect()
return
}
}
if let error = error {
- print("HotlineTracker: receive error \(error)")
+ print("HotlineTrackerClient: receive error \(error)")
self.disconnect()
}
else {
- print("HotlineTracker: not complete")
+ print("HotlineTrackerClient: not complete")
self.receiveListing()
}
}
@@ -258,12 +226,13 @@ class HotlineTrackerClient {
// Description size (1 byte)
// Description (description size)
+ let trackerSeparatorRegex = /^[-]+$/
var foundServers: [HotlineServer] = []
var cursor = 0
for _ in 1...self.serverCount {
if self.bytes.count < cursor + 12 {
- print("HotlineTracker: Data isn't long enough for next server")
+ print("HotlineTrackerClient: Data isn't long enough for next server")
break
}
@@ -281,19 +250,18 @@ 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)
- foundServers.append(server)
+ let validName = try? trackerSeparatorRegex.prefixMatch(in: name)
+ if validName == nil {
+// if name.range(of: regex, options: .regularExpression) == nil {
+ let server = HotlineServer(address: "\(ip_1).\(ip_2).\(ip_3).\(ip_4)", port: port, users: userCount, name: name, description: desc)
+ foundServers.append(server)
+ }
}
cursor += 10 + nameByteCount + descByteCount
}
}
- DispatchQueue.main.async {
- self.tracker.servers = foundServers
-// print("CALLING CALLBACK")
- self.servers = foundServers
- }
-
+ self.servers = foundServers
}
}
diff --git a/Hotline/HotlineApp.swift b/Hotline/HotlineApp.swift
index 82fa7d4..67397a2 100644
--- a/Hotline/HotlineApp.swift
+++ b/Hotline/HotlineApp.swift
@@ -18,7 +18,6 @@ struct HotlineApp: App {
@State private var appState = HotlineState()
@State private var hotline = HotlineClient()
- @State private var tracker = HotlineTrackerClient(tracker: HotlineTracker("hltracker.com"))
private var model = Hotline(trackerClient: HotlineTrackerClient())
@@ -28,7 +27,8 @@ struct HotlineApp: App {
TrackerView()
.environment(appState)
.environment(hotline)
- .environment(tracker)
+// .environment(tracker)
+ .environment(model)
}
// .modelContainer(sharedModelContainer)
}
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index 70f68a9..b83dc80 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -3,19 +3,21 @@ import SwiftUI
@Observable final class Hotline {
let trackerClient: HotlineTrackerClient
-
-
init(trackerClient: HotlineTrackerClient) {
self.trackerClient = trackerClient
}
- @MainActor func getServers(address: String, port: Int) async {
- let fetchedServers = await withCheckedContinuation { [weak self] continuation in
- self?.trackerClient.fetch() {
- continuation.resume(returning: [])
+ @MainActor func getServers(address: String, port: Int = Tracker.defaultPort) async -> [Server] {
+ let fetchedServers: [HotlineServer] = await self.trackerClient.fetchServers(address: address, port: port)
+
+ var servers: [Server] = []
+
+ for s in fetchedServers {
+ if let serverName = s.name {
+ servers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users)))
}
}
-
+ return servers
}
}
diff --git a/Hotline/Models/Server.swift b/Hotline/Models/Server.swift
index c2063ea..5a6ba80 100644
--- a/Hotline/Models/Server.swift
+++ b/Hotline/Models/Server.swift
@@ -1,16 +1,26 @@
import SwiftUI
-@Observable final class Server {
+@Observable final class Server: Identifiable, Equatable {
+ let id: UUID = UUID()
let name: String
let description: String?
- let users: Int = 0
+ let users: Int
let address: String
let port: Int
- init(name: String, description: String?, address: String, port: Int) {
+ init(name: String, description: String?, address: String, port: Int, users: Int = 0) {
self.name = name
self.description = description
self.address = address
self.port = port
+ self.users = users
+ }
+
+ static func == (lhs: Server, rhs: Server) -> Bool {
+ return lhs.id == rhs.id
+ }
+
+ static func == (lhs: HotlineServer, rhs: Server) -> Bool {
+ return lhs.name == rhs.name && lhs.address == rhs.address && lhs.port == rhs.port
}
}
diff --git a/Hotline/Models/Tracker.swift b/Hotline/Models/Tracker.swift
index c9dfb5c..66d7976 100644
--- a/Hotline/Models/Tracker.swift
+++ b/Hotline/Models/Tracker.swift
@@ -1,20 +1,22 @@
import SwiftUI
@Observable final class Tracker {
+ static let defaultPort: Int = 5498
+
let service: HotlineTrackerClient
let address: String
let port: Int
var servers: [Server] = []
- init(address: String, port: Int = 5498, service: HotlineTrackerClient) {
+ init(address: String, port: Int = defaultPort, service: HotlineTrackerClient) {
self.address = address
self.port = port
self.service = service
}
func fetchServers() {
- self.service.fetch2(address: self.address, port: self.port) { servers in
- self.servers = servers
- }
+// self.service.fetch2(address: self.address, port: self.port) { hotlineServers in
+// self.servers = servers
+// }
}
}
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index 000314e..719df40 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -5,6 +5,24 @@ enum Endianness {
case little
}
+func detectStringEncoding(of data: Data) -> String.Encoding {
+// var nsString: NSString? = nil
+// guard case let rawValue = NSString.stringEncoding(for: data, encodingOptions: [.allowLossyKey: false], convertedString: nil, usedLossyConversion: nil) else {
+// print("NO ENCODING")
+// return nil
+// }
+
+ let rawValue = NSString.stringEncoding(for: data, encodingOptions: [.allowLossyKey: false], convertedString: nil, usedLossyConversion: nil)
+
+// let cfEnc: CFStringEncoding = CFStringConvertNSStringEncodingToEncoding(rawValue);
+
+// let encodingString: CFString? = CFStringGetNameOfEncoding(cfEnc)
+
+ print("DETECTED ENCODING \(rawValue)")
+
+ return String.Encoding(rawValue: rawValue)
+}
+
extension Data {
init(_ val: UInt8) {
self.init()
@@ -50,16 +68,37 @@ extension Data {
}
return self.subdata(in: offset..<(offset + length))
}
-
+
func readString(at offset: Int, length: Int) -> String? {
- var str: String?
+ let subdata = self[offset..<(offset + length)]
+ if subdata.count == 0 {
+ return ""
+ }
+
+ let allowedEncodings = [
+ NSUTF8StringEncoding,
+ NSShiftJISStringEncoding,
+ NSUnicodeStringEncoding,
+ NSWindowsCP1251StringEncoding
+ ]
+
+ var decodedNSString: NSString?
+ let rawValue = NSString.stringEncoding(for: subdata, encodingOptions: [.allowLossyKey: false], convertedString: &decodedNSString, usedLossyConversion: nil)
+
+ if allowedEncodings.contains(rawValue) {
+ return decodedNSString as? String
+ }
+
+ else if rawValue > 1 {
+ print("ENCODING FOUND \(rawValue)")
+ }
- str = String(data: self[offset..<(offset + length)], encoding: .utf8)
- if str == nil {
- str = String(data: self[offset..<(offset + length)], encoding: .ascii)
+ var macStr = String(data: subdata, encoding: .macOSRoman)
+ if macStr == nil {
+ macStr = String(data: subdata, encoding: .nonLossyASCII)
}
- return str
+ return macStr
}
func readPString(at offset: Int) -> (String?, Int) {
diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift
index 28e548d..1b3ba52 100644
--- a/Hotline/Views/ChatView.swift
+++ b/Hotline/Views/ChatView.swift
@@ -25,12 +25,27 @@ struct ChatView: View {
ForEach(hotline.chatMessages) { msg in
if msg.type == .agreement {
VStack(alignment: .leading) {
- Text(msg.text)
- .font(.system(size: 11, weight: .regular, design: .monospaced))
+ VStack(alignment: .leading, spacing: 0) {
+ Text(msg.text)
+ .textSelection(.enabled)
+ .padding()
+ .opacity(0.75)
+ HStack {
+ Spacer()
+ Text((hotline.server?.name ?? "") + " Server Agreement")
+ .font(.caption)
+ .fontWeight(.medium)
+ .opacity(0.4)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ Spacer()
+ }
.padding()
- .opacity(0.75)
- .background(colorScheme == .dark ? Color(white: 0.1) : Color(white: 0.96))
- .cornerRadius(16)
+ .background(colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.9))
+ }
+ .background(colorScheme == .dark ? Color(white: 0.1) : Color(white: 0.96))
+ .cornerRadius(16)
+ .frame(maxWidth: .infinity)
}
.padding()
}
diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift
index 091d718..23cd914 100644
--- a/Hotline/Views/FilesView.swift
+++ b/Hotline/Views/FilesView.swift
@@ -4,144 +4,6 @@ import UniformTypeIdentifiers
struct FileView: View {
@Environment(HotlineClient.self) private var hotline
- @State var file: HotlineFile
- @State var loaded = false
- @State var fileList: [HotlineFile] = []
-
- let depth: Int
- let path: [String]
-
- static let byteFormatter = ByteCountFormatter()
-
- private func formattedFileSize(_ fileSize: UInt32) -> String {
- // let bcf = ByteCountFormatter()
- FileView.byteFormatter.allowedUnits = [.useAll]
- FileView.byteFormatter.countStyle = .file
- return FileView.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 {
- if file.isFolder {
- DisclosureGroup {
- if !loaded {
- Text("Loading...")
- .task {
- if !loaded {
- hotline.sendGetFileList(path: path) {
- print("FETCHED!")
- } reply: { newFiles in
- print("GOT FILES REPLY?", newFiles)
-
- fileList = newFiles
- loaded = true
- }
- }
- }
- }
- else {
- FileListView(fileList: fileList, depth: depth + 1, path: path + [file.name])
- }
- } label: {
- VStack {
- HStack {
- HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
- Image(systemName: "folder.fill")
- }
- .frame(minWidth: 25)
- Text(file.name).fontWeight(.medium)
- Spacer()
- Text("\(file.fileSize)").foregroundStyle(.secondary)
- }
- }
- .frame(height: 44)
- .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
-
- // Divider()
- // .padding(EdgeInsets(top: 0, leading: 16 + 25 + 8, bottom: 0, trailing: 0))
- }
- .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: depth == 0 ? 16 : 0))
- .background(Color(uiColor: UIColor.systemBackground))
- }
- else {
- VStack {
- 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)
- }
- .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0))
- }
- .frame(height: 44)
- .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: depth == 0 ? 16 : 0))
- .background(Color(uiColor: UIColor.systemBackground))
-
- Divider()
- .padding(EdgeInsets(top: 0, leading: 16 + 25 + 8, bottom: 0, trailing: 0))
- }
- }
-}
-
-struct FileListView: View {
- @Environment(HotlineClient.self) private var hotline
-
- @State private var fetched = false
- @State private var loaded = false
- @State var fileList: [HotlineFile] = []
- @State var expanded = false
-
- var depth = 0
-
- var path: [String] = []
-
- var body: some View {
- LazyVStack(alignment: .leading, spacing: 0) {
- Section {
- ForEach(fileList, id: \.self) { file in
- FileView(file: file, depth: depth, path: path + [file.name])
- }
- }
- Spacer()
- }
- .listStyle(.plain)
- .animation(.default, value: fileList)
- }
-}
-
-struct BestFileView: View {
- @Environment(HotlineClient.self) private var hotline
-
@State var expanded = false
var file: HotlineFile
@@ -150,7 +12,7 @@ struct BestFileView: View {
if file.isFolder {
DisclosureGroup(isExpanded: $expanded) {
ForEach(file.files!) { childFile in
- BestFileView(file: childFile)
+ FileView(file: childFile)
.frame(height: 44)
}
} label: {
@@ -159,9 +21,9 @@ struct BestFileView: View {
Image(systemName: "folder.fill")
}
.frame(minWidth: 25)
- Text(file.name).fontWeight(.medium)
+ Text(file.name).fontWeight(.medium).lineLimit(1).truncationMode(.tail)
Spacer()
- Text("\(file.fileSize)").foregroundStyle(.secondary)
+ Text("\(file.fileSize)").foregroundStyle(.secondary).lineLimit(1)
}
}
.onChange(of: expanded) {
@@ -174,11 +36,12 @@ struct BestFileView: View {
HStack {
HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
Image(uiImage: fileIcon(name: file.name))
+ .renderingMode(.template)
}
.frame(minWidth: 25)
- Text(file.name).fontWeight(.medium)
+ Text(file.name).lineLimit(1).truncationMode(.tail)
Spacer()
- Text(formattedFileSize(file.fileSize)).foregroundStyle(.secondary)
+ Text(formattedFileSize(file.fileSize)).foregroundStyle(.secondary).lineLimit(1)
}
}
}
@@ -187,9 +50,9 @@ struct BestFileView: View {
private func formattedFileSize(_ fileSize: UInt32) -> String {
// let bcf = ByteCountFormatter()
- BestFileView.byteFormatter.allowedUnits = [.useAll]
- BestFileView.byteFormatter.countStyle = .file
- return BestFileView.byteFormatter.string(fromByteCount: Int64(fileSize))
+ FileView.byteFormatter.allowedUnits = [.useAll]
+ FileView.byteFormatter.countStyle = .file
+ return FileView.byteFormatter.string(fromByteCount: Int64(fileSize))
}
private func fileIcon(name: String) -> UIImage {
@@ -228,7 +91,7 @@ struct FilesView: View {
NavigationStack {
List(hotline.fileList) { file in
// OutlineGroup(hotline.fileList, children: \.files, expanded: $expandedFolders) { file in
- BestFileView(file: file)
+ FileView(file: file)
.frame(height: 44)
// }
}
diff --git a/Hotline/Views/HotlineView.swift b/Hotline/Views/HotlineView.swift
deleted file mode 100644
index f950dd7..0000000
--- a/Hotline/Views/HotlineView.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-import SwiftUI
-
-struct HotlineView: View {
- @Environment(HotlineState.self) private var appState
- @Environment(HotlineClient.self) private var hotline
- @Environment(HotlineTrackerClient.self) private var tracker
-
- var body: some View {
- TrackerView()
- }
-}
-
-#Preview {
- HotlineView()
- .environment(HotlineClient())
- .environment(HotlineTrackerClient(tracker: HotlineTracker("hltracker.com")))
-}
diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift
index 1c61cf4..2d2032e 100644
--- a/Hotline/Views/TrackerView.swift
+++ b/Hotline/Views/TrackerView.swift
@@ -5,23 +5,23 @@ struct TrackerView: View {
// @Environment(\.modelContext) private var modelContext
// @Query private var items: [Item]
- @Environment(HotlineState.self) private var appState
@Environment(HotlineClient.self) private var hotline
- @Environment(HotlineTrackerClient.self) private var trackerClient
@Environment(Hotline.self) private var model: Hotline
@Environment(\.colorScheme) var colorScheme
// @State private var tracker = Tracker(address: "hltracker.com", service: trackerService)
- @State private var selectedServer: HotlineServer?
+ @State private var servers: [Server] = []
+ @State private var selectedServer: Server?
@State var scrollOffset: CGFloat = CGFloat.zero
+ @State private var initialLoadComplete = false
- func shouldDisplayDescription(server: HotlineServer) -> Bool {
- guard let name = server.name, let desc = server.description else {
+ func shouldDisplayDescription(server: Server) -> Bool {
+ guard let desc = server.description else {
return false
}
- return desc.count > 0 && desc != name && !desc.contains(/^-+/)
+ return desc.count > 0 && desc != server.name
}
func connectionStatusToProgress(status: HotlineClientStatus) -> Double {
@@ -43,10 +43,11 @@ struct TrackerView: View {
return (v - lower) / (upper - lower)
}
+ func updateServers() async {
+ self.servers = await model.getServers(address: "tracker.preterhuman.net")
+ }
+
var body: some View {
- @Bindable var config = appState
- @Bindable var client = hotline
-
ZStack(alignment: .center) {
VStack(alignment: .center) {
ZStack(alignment: .top) {
@@ -80,18 +81,18 @@ struct TrackerView: View {
}
ObservableScrollView(scrollOffset: $scrollOffset) {
LazyVStack(alignment: .leading) {
- ForEach(tracker.servers) { server in
+ ForEach(self.servers) { server in
VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "globe.americas.fill").font(.title3)
VStack(alignment: .leading) {
- Text(server.name!).font(.title3).fontWeight(.medium)
+ Text(server.name).font(.title3).fontWeight(.medium)
if shouldDisplayDescription(server: server) {
Spacer()
Text(server.description!).opacity(0.5).font(.system(size: 16))
}
Spacer()
- Text("\(server.address)").opacity(0.3).font(.system(size: 13))
+ Text("\(server.address):" + String(format: "%i", server.port)).opacity(0.3).font(.system(size: 13))
}
Spacer()
if server.users > 0 {
@@ -101,14 +102,14 @@ struct TrackerView: View {
if server == selectedServer {
Spacer(minLength: 16)
- if hotline.server == server && hotline.connectionStatus != .disconnected {
+ if hotline.connectionStatus != .disconnected && hotline.server! == server {
ProgressView(value: connectionStatusToProgress(status: hotline.connectionStatus))
.frame(minHeight: 10)
.accentColor(colorScheme == .dark ? .white : .black)
}
else {
Button("Connect") {
- hotline.connect(to: server)
+ hotline.connect(to: HotlineServer(address: server.address, port: UInt16(server.port), users: UInt16(server.users), name: server.name, description: server.description))
}
.bold()
.padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24))
@@ -142,13 +143,19 @@ struct TrackerView: View {
}
.padding(EdgeInsets(top: 75, leading: 0, bottom: 0, trailing: 0))
}
- .refreshable {
- await withCheckedContinuation { continuation in
- tracker.fetch() {
- continuation.resume()
+ .overlay {
+ if !initialLoadComplete {
+ VStack {
+ ProgressView()
+ .controlSize(.large)
}
+ .frame(maxWidth: .infinity)
}
}
+ .refreshable {
+ initialLoadComplete = true
+ await updateServers()
+ }
}
.fullScreenCover(isPresented: Binding(get: { return hotline.connectionStatus == .loggedIn }, set: { _ in }), onDismiss: {
@@ -159,7 +166,9 @@ struct TrackerView: View {
.background(Color(uiColor: UIColor.systemGroupedBackground))
.frame(maxWidth: .infinity)
.task {
- tracker.fetch()
+ await updateServers()
+ initialLoadComplete = true
+// tracker.fetch()
}
}
}
@@ -167,7 +176,6 @@ struct TrackerView: View {
#Preview {
TrackerView()
.environment(HotlineClient())
- .environment(HotlineTrackerClient(tracker: HotlineTracker("hltracker.com")))
.environment(HotlineState())
// .modelContainer(for: Item.self, inMemory: true)
}