diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-12-11 23:32:28 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-12-11 23:32:28 -0800 |
| commit | 18c4b8643df09be4ce52a4b110ea21ac1ad053fc (patch) | |
| tree | 335d4f0a6b430b983b6f00567b91eb1ee6b16b4b /Hotline/Models | |
| parent | 84aaa9ef0a1be6986e38d7e4f58e07d3cb84979e (diff) | |
Move to new Hotline model for data observation and let client exist standalone.
Diffstat (limited to 'Hotline/Models')
| -rw-r--r-- | Hotline/Models/Hotline.swift | 303 | ||||
| -rw-r--r-- | Hotline/Models/Server.swift | 2 | ||||
| -rw-r--r-- | Hotline/Models/Tracker.swift | 11 |
3 files changed, 304 insertions, 12 deletions
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index b83dc80..3272eb3 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -1,12 +1,127 @@ import SwiftUI -@Observable final class Hotline { +@Observable class FileInfo: Identifiable { + let id: UUID = UUID() + + let path: [String] + let name: String + + let type: String + let creator: String + let fileSize: UInt + + let isFolder: Bool + var children: [FileInfo]? = nil + + init(hotlineFile: HotlineFile) { + self.path = hotlineFile.path + self.name = hotlineFile.name + self.type = hotlineFile.type + self.creator = hotlineFile.creator + self.fileSize = UInt(hotlineFile.fileSize) + self.isFolder = hotlineFile.isFolder + if self.isFolder { + self.children = [] + } + } + + static func == (lhs: FileInfo, rhs: FileInfo) -> Bool { + return lhs.id == rhs.id + } +} + +enum ChatMessageType { + case agreement + case status + case message + case server +} + +struct ChatMessage: Identifiable { + let id = UUID() + + let text: String + let type: ChatMessageType + let date: Date + let username: String? + + static let parser = /^\s*([^\:]+)\:\s*(.+)/ + + init(text: String, type: ChatMessageType, date: Date) { + self.type = type + self.date = date + + if + type == .message, + let match = text.firstMatch(of: ChatMessage.parser) { + self.username = String(match.1) + self.text = String(match.2) + } + else { + self.username = nil + self.text = text + } + } +} + +struct UserStatus: OptionSet { + let rawValue: Int + + static let idle = UserStatus(rawValue: 1 << 0) + static let admin = UserStatus(rawValue: 1 << 1) +} + +struct User: Identifiable { + let id: UInt + var name: String + var iconID: UInt + var status: UserStatus + + init(hotlineUser: HotlineUser) { + var status: UserStatus = UserStatus() + if hotlineUser.isIdle { status.update(with: .idle) } + if hotlineUser.isAdmin { status.update(with: .admin) } + + self.id = UInt(hotlineUser.id) + self.name = hotlineUser.name + self.iconID = UInt(hotlineUser.iconID) + self.status = status + } + + init(id: UInt, name: String, iconID: UInt, status: UserStatus) { + self.id = id + self.name = name + self.iconID = iconID + self.status = status + } +} + +@Observable final class Hotline: HotlineNewClientDelegate { let trackerClient: HotlineTrackerClient + let client: HotlineNewClient + + var status: HotlineNewClientStatus = .disconnected + + var server: Server? = nil + var serverVersion: UInt16? = nil + var username: String = "bolt" + var iconID: UInt = 128 - init(trackerClient: HotlineTrackerClient) { + var users: [User] = [] + var chat: [ChatMessage] = [] + var messageBoard: [String] = [] + var files: [FileInfo] = [] + + // MARK: - + + init(trackerClient: HotlineTrackerClient, client: HotlineNewClient) { self.trackerClient = trackerClient + self.client = client + self.client.delegate = self } + // MARK: - + @MainActor func getServers(address: String, port: Int = Tracker.defaultPort) async -> [Server] { let fetchedServers: [HotlineServer] = await self.trackerClient.fetchServers(address: address, port: port) @@ -20,4 +135,188 @@ import SwiftUI return servers } + + @MainActor func disconnectTracker() { + self.trackerClient.disconnect() + } + + @MainActor func login(server: Server, login: String, password: String, username: String, iconID: UInt) async -> Bool { + self.server = server + self.username = username + self.iconID = iconID + + return await withCheckedContinuation { [weak self] continuation in + let _ = self?.client.login(server.address, port: UInt16(server.port), login: login, password: password, username: username, iconID: UInt16(iconID)) { [weak self] err, serverVersion in + self?.serverVersion = serverVersion + continuation.resume(returning: (err != nil)) + } + } + } + + @MainActor func disconnect() { + self.client.disconnect() + } + + @MainActor func sendChat(_ text: String) { + self.client.sendChat(message: text, sent: nil) + } + + @MainActor func getMessageBoard() async -> [String] { + self.messageBoard = await withCheckedContinuation { [weak self] continuation in + self?.client.sendGetMessageBoard() { err, messages in + continuation.resume(returning: (err != nil ? [] : messages)) + } + } + + return self.messageBoard + } + + @MainActor func getFileList(path: [String] = []) async -> [FileInfo] { + return await withCheckedContinuation { [weak self] continuation in + self?.client.sendGetFileList(path: path, sent: { success in + if !success { + continuation.resume(returning: []) + return + } + // Failed to send? + }, reply: { [weak self] files in + let parentFile = self?.findFile(in: self?.files ?? [], at: path) + + var newFiles: [FileInfo] = [] + for f in files { + newFiles.append(FileInfo(hotlineFile: f)) + } + + if let parent = parentFile { + print("FOUND PARENT AT \(path)") + parent.children = newFiles + } + else if path.isEmpty { + print("FOUND ROOT AT \(path)") + self?.files = newFiles + } + + continuation.resume(returning: newFiles) + }) + } + } + + +// @MainActor func updateUsers() async -> [User] { +// let userList = await self.client.sendGetUserList() +// var users = [] +//// self.client.sendChat(message: text) +// +// return users +// } + + // MARK: - Hotline Delegate + + func hotlineStatusChanged(status: HotlineNewClientStatus) { + print("Hotline: Connection status changed to: \(status)") + + if status == .disconnected { + self.serverVersion = nil + self.users = [] + self.chat = [] + self.messageBoard = [] + self.files = [] + } + + self.status = status + } + + func hotlineGetUserInfo() -> (String, UInt16) { + return (self.username, UInt16(self.iconID)) + } + + func hotlineReceivedAgreement(text: String) { + self.chat.append(ChatMessage(text: text, type: .agreement, date: Date())) + } + + func hotlineReceivedServerMessage(message: String) { +// print("Hotline: received server message:\n\(message)") +// self.chat.append(ChatMessage(text: message, type: .server, date: Date())) + } + + func hotlineReceivedChatMessage(message: String) { + self.chat.append(ChatMessage(text: message, type: .message, date: Date())) + } + + func hotlineReceivedUserList(users: [HotlineUser]) { + var existingUserIDs: [UInt] = [] + var userList: [User] = [] + + print("GOT USER LIST", users) + + for u in users { + if let i = self.users.firstIndex(where: { $0.id == u.id }) { + // If a user is already in the user list we have to assume + // they changed somehow before we received the user list + // which means let's keep their existing info. + existingUserIDs.append(UInt(u.id)) + userList.append(self.users[i]) + } + else { + userList.append(User(hotlineUser: u)) + } + } + + if !existingUserIDs.isEmpty { + self.users = self.users.filter { !existingUserIDs.contains($0.id) } + } + + self.users = userList + self.users + } + + func hotlineUserChanged(user: HotlineUser) { + self.addOrUpdateHotlineUser(user) + } + + func hotlineUserDisconnected(userID: UInt16) { + if let existingUserIndex = self.users.firstIndex(where: { $0.id == UInt(userID) }) { + let user = self.users.remove(at: existingUserIndex) + self.chat.append(ChatMessage(text: "\(user.name) left", type: .status, date: Date())) + } + } + + func hotlineReceivedError(message: String) { + + } + + // MARK: - Utilities + + private func addOrUpdateHotlineUser(_ user: HotlineUser) { + if let i = self.users.firstIndex(where: { $0.id == user.id }) { + print("Hotline: updating user \(self.users[i].name)") + self.users[i] = User(hotlineUser: user) + } + else { + print("Hotline: added user: \(user.name)") + self.users.append(User(hotlineUser: user)) + self.chat.append(ChatMessage(text: "\(user.name) joined", type: .status, date: Date())) + } + } + + private func findFile(in filesToSearch: [FileInfo], at path: [String]) -> FileInfo? { + guard !path.isEmpty, !filesToSearch.isEmpty else { return nil } + + // var stack: [([HotlineFile], [String])] = [(self.files!, path)] + + let currentName = path[0] + + for file in filesToSearch { + if file.name == currentName { + if path.count == 1 { + return file + } + else if let subfiles = file.children { + let remainingPath = Array(path[1...]) + return self.findFile(in: subfiles, at: remainingPath) + } + } + } + + return nil + } } diff --git a/Hotline/Models/Server.swift b/Hotline/Models/Server.swift index 5a6ba80..b159a1a 100644 --- a/Hotline/Models/Server.swift +++ b/Hotline/Models/Server.swift @@ -1,6 +1,8 @@ import SwiftUI @Observable final class Server: Identifiable, Equatable { + static let defaultPort: Int = 5500 + let id: UUID = UUID() let name: String let description: String? diff --git a/Hotline/Models/Tracker.swift b/Hotline/Models/Tracker.swift index 66d7976..f2111a0 100644 --- a/Hotline/Models/Tracker.swift +++ b/Hotline/Models/Tracker.swift @@ -3,20 +3,11 @@ 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 = defaultPort, service: HotlineTrackerClient) { + init(address: String, port: Int = defaultPort) { self.address = address self.port = port - self.service = service - } - - func fetchServers() { -// self.service.fetch2(address: self.address, port: self.port) { hotlineServers in -// self.servers = servers -// } } } |