diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-12-04 09:12:58 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-12-04 09:12:58 -0800 |
| commit | 471546236b8991f61d26c0e3aa8ee48b83b983af (patch) | |
| tree | b74331a8c8ea61ee7e1eaedfc7bbd1686bbc4702 /Hotline | |
| parent | 885119acdd27bd53ed3096285c3eca3e2d99ad70 (diff) | |
Starting to hash out a server view and tabs. Added research pdfs. etc.
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift (renamed from Hotline/Network/HotlineClient.swift) | 47 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineClientModel.swift | 10 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineProtocol.swift (renamed from Hotline/Network/HotlineProtocol.swift) | 20 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineTrackerClient.swift (renamed from Hotline/Network/HotlineTracker.swift) | 45 | ||||
| -rw-r--r-- | Hotline/HotlineApp.swift | 8 | ||||
| -rw-r--r-- | Hotline/TrackerView.swift | 100 | ||||
| -rw-r--r-- | Hotline/Views/AgreementView.swift | 35 | ||||
| -rw-r--r-- | Hotline/Views/ChatView.swift | 63 | ||||
| -rw-r--r-- | Hotline/Views/HotlineView.swift | 20 | ||||
| -rw-r--r-- | Hotline/Views/ServerView.swift | 62 | ||||
| -rw-r--r-- | Hotline/Views/TrackerServerView.swift | 47 | ||||
| -rw-r--r-- | Hotline/Views/TrackerView.swift | 73 |
12 files changed, 376 insertions, 154 deletions
diff --git a/Hotline/Network/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index b5092a0..b5ea1d9 100644 --- a/Hotline/Network/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -9,8 +9,15 @@ enum HotlineClientStatus: Int { case loggedIn } -class HotlineClient : ObservableObject { - static let shared = HotlineClient() +struct HotlineChat { + let message: String + let userID: UInt16? + let isTopic: Bool +} + +@Observable +class HotlineClient { +// static let shared = HotlineClient() static let handshakePacket = Data([ 0x54, 0x52, 0x54, 0x50, // 'TRTP' protocol ID @@ -19,18 +26,19 @@ class HotlineClient : ObservableObject { 0x00, 0x02, // Sub-version ]) - @Published var connectionStatus: HotlineClientStatus = .disconnected - @Published var agreement: String? - @Published var userList: [HotlineUser] = [] - @Published var chatMessages: [String] = [] + var connectionStatus: HotlineClientStatus = .disconnected + var agreement: String? + var users: [UInt16:HotlineUser] = [:] + var userList: [UInt16] = [] + var chatMessages: [HotlineChat] = [] var userName: String = "bolt" var userIconID: UInt16 = 128 var serverVersion: UInt16 = 151 var server: HotlineServer? - var connection: NWConnection? - private var transactionLog: [UInt32:HotlineTransactionType] = [:] + @ObservationIgnored private var connection: NWConnection? + @ObservationIgnored private var transactionLog: [UInt32:HotlineTransactionType] = [:] init() { @@ -347,15 +355,19 @@ class HotlineClient : ObservableObject { } case .getUserNameList: print("GOT USER LIST") - var newUserList: [HotlineUser] = [] + var newUsers: [UInt16:HotlineUser] = [:] + var newUserList: [UInt16] = [] 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) + let user = u.getUser() + newUsers[user.id] = user + newUserList.append(user.id) } DispatchQueue.main.async { + self.users = newUsers self.userList = newUserList + + print("HotlineClient got users:\n") + print("\(self.userList)\n\n") } default: break @@ -386,16 +398,9 @@ class HotlineClient : ObservableObject { let userID = userIDParam.getUInt16() { print("HotlineClient: \(userName):\(userID): \(chatText)") DispatchQueue.main.async { - self.chatMessages.append(chatText) + self.chatMessages.append(HotlineChat(message: chatText, userID: userID, isTopic: false)) } } - case .getUserNameList: - print("HotlineClient: GOT USER NAME LIST!") - 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.getField(type: .userName), diff --git a/Hotline/Hotline/HotlineClientModel.swift b/Hotline/Hotline/HotlineClientModel.swift new file mode 100644 index 0000000..a45a5e2 --- /dev/null +++ b/Hotline/Hotline/HotlineClientModel.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@Observable +class HotlineClientModel { + var status: HotlineClientStatus = .disconnected + var server: HotlineServer? + var userlist: [HotlineUser]? + var chat: [(HotlineUser, String)]? + var agreement: String? +} diff --git a/Hotline/Network/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift index 5372e78..200ac47 100644 --- a/Hotline/Network/HotlineProtocol.swift +++ b/Hotline/Hotline/HotlineProtocol.swift @@ -73,22 +73,6 @@ struct HotlineUser: Identifiable, Hashable { } } -struct HotlineUserInfo { - let id: UInt16 - let iconID: UInt16 - let flags: UInt16 - let userName: String - - init(data: Data) { - self.id = data.readUInt16(at: 0)! - self.iconID = data.readUInt16(at: 2)! - self.flags = data.readUInt16(at: 4)! - - let userNameLength = data.readUInt16(at: 6)! - self.userName = data.readString(at: 8, length: Int(userNameLength), encoding: .ascii)! - } -} - struct HotlineTransactionField { let type: HotlineTransactionFieldType let dataSize: UInt16 @@ -171,8 +155,8 @@ struct HotlineTransactionField { return String(data: self.data, encoding: encoding) } - func getUserInfo() -> HotlineUserInfo { - return HotlineUserInfo(data: self.data) + func getUser() -> HotlineUser { + return HotlineUser(from: self.data) } } diff --git a/Hotline/Network/HotlineTracker.swift b/Hotline/Hotline/HotlineTrackerClient.swift index 2c44bc8..fd4e97b 100644 --- a/Hotline/Network/HotlineTracker.swift +++ b/Hotline/Hotline/HotlineTrackerClient.swift @@ -7,26 +7,42 @@ enum HotlineTrackerStatus: Int { case connected } -class HotlineTracker : ObservableObject { - let serverAddress: NWEndpoint.Host - let serverPort: NWEndpoint.Port = NWEndpoint.Port(rawValue: 5498)! +struct HotlineTracker: Identifiable { + 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 + } +} + +@Observable +class HotlineTrackerClient { static let magicPacket = Data([ 0x48, 0x54, 0x52, 0x4B, // 'HTRK' 0x00, 0x01 // Version ]) - var connection: NWConnection? - - var bytes = Data() - var maxDataLength: Int = 0 - var serverCount: Int = 0 + @ObservationIgnored private let serverAddress: NWEndpoint.Host + @ObservationIgnored private let 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 - @Published var connectionStatus: HotlineTrackerStatus = .disconnected - @Published var servers: [HotlineServer] = [] + var tracker: HotlineTracker + var connectionStatus: HotlineTrackerStatus = .disconnected + var servers: [HotlineServer] = [] - init(address: String) { - self.serverAddress = NWEndpoint.Host(address) + init(tracker: HotlineTracker) { + self.tracker = tracker + self.serverAddress = NWEndpoint.Host(tracker.address) + self.serverPort = NWEndpoint.Port(rawValue: tracker.port)! } func fetch() { @@ -88,7 +104,7 @@ class HotlineTracker : ObservableObject { // let packet: [UInt8] = [0x48, 0x54, 0x52, 0x4B, 0x00, self.serverVersion] - c.send(content: HotlineTracker.magicPacket, completion: .contentProcessed { [weak self] (error) in + c.send(content: HotlineTrackerClient.magicPacket, completion: .contentProcessed { [weak self] (error) in if let err = error { print("HotlineTracker: sending magic failed \(err)") return @@ -112,7 +128,7 @@ class HotlineTracker : ObservableObject { return } - if data.isEmpty || !data.elementsEqual(HotlineTracker.magicPacket) { + if data.isEmpty || !data.elementsEqual(HotlineTrackerClient.magicPacket) { print("HotlineTracker: invalid magic response") self.disconnect() return @@ -252,6 +268,7 @@ class HotlineTracker : ObservableObject { } DispatchQueue.main.async { + self.tracker.servers = foundServers // print("CALLING CALLBACK") self.servers = foundServers } diff --git a/Hotline/HotlineApp.swift b/Hotline/HotlineApp.swift index da49fd0..238ca48 100644 --- a/Hotline/HotlineApp.swift +++ b/Hotline/HotlineApp.swift @@ -16,9 +16,15 @@ struct HotlineApp: App { // } // }() + @State private var hotline = HotlineClient() + @State private var tracker = HotlineTrackerClient(tracker: HotlineTracker("hltracker.com")) + var body: some Scene { + WindowGroup { - TrackerView() + HotlineView() + .environment(hotline) + .environment(tracker) } // .modelContainer(sharedModelContainer) } diff --git a/Hotline/TrackerView.swift b/Hotline/TrackerView.swift deleted file mode 100644 index 1b5cce5..0000000 --- a/Hotline/TrackerView.swift +++ /dev/null @@ -1,100 +0,0 @@ -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") - @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() - Text("\(server.users)").opacity(0.6) - } - .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) - } - } - .background(Color(white: 0.96)) - .listStyle(.plain) - .frame(maxWidth: .infinity) - .task { - tracker.fetch() - } - .sheet(item: $selectedServer) { item in - TrackerServerView(server: item) - } - .sheet(isPresented: Binding(get: { client.agreement != nil }, set: { _ in })) { - AgreementView(text: client.agreement!) - } - } -} - -#Preview { - TrackerView() - // .modelContainer(for: Item.self, inMemory: true) -} diff --git a/Hotline/Views/AgreementView.swift b/Hotline/Views/AgreementView.swift new file mode 100644 index 0000000..c61c9d7 --- /dev/null +++ b/Hotline/Views/AgreementView.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct AgreementView: View { + @Environment(\.dismiss) var dismiss + + let text: String + + var body: some View { + VStack(alignment: .leading) { + ScrollView { + VStack(alignment: .leading) { + Text(text) + .fontDesign(.monospaced) + .padding() + .dynamicTypeSize(.small) + .textSelection(.enabled) + } + } + Button("OK") { + print("DONE") + } + .bold() + .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) + .frame(maxWidth: .infinity) + } + } +} + +#Preview { + AgreementView(text: """ +Welcome! + +Take it on real one. +""") +} diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift new file mode 100644 index 0000000..532a890 --- /dev/null +++ b/Hotline/Views/ChatView.swift @@ -0,0 +1,63 @@ +import SwiftUI + +struct ChatView: View { +// @Binding private var input: String + @State var input: String = "" + @State private var scrollPos: Int? + @State private var contentHeight: CGFloat = 0 + + var body: some View { + VStack(spacing: 0) { + GeometryReader { geometry in + ScrollView(.vertical) { + ScrollViewReader { scrollReader in + VStack(alignment: .leading) { + Spacer() + LazyVStack(alignment: .leading, spacing: 10) { + HStack(alignment: .firstTextBaseline) { + Text("bolt:").bold().fontDesign(.monospaced).frame(minWidth: 60) + Text("hello!").fontDesign(.monospaced).textSelection(.enabled) + } + ForEach(0..<50) { i in + HStack(alignment: .firstTextBaseline) { + Text("mierau:").bold().fontDesign(.monospaced) + Text("g'day to you. what's going on this afternoon?") + .fontDesign(.monospaced) + .textSelection(.enabled) + } + } + Text("").font(.system(size: 0)).id("bottomScroll") + } + .padding() + } + // .frame(width: geometry.size.width) + .frame(minHeight: geometry.size.height) +// .background(Color.red) + .onAppear() { + scrollReader.scrollTo("bottomScroll", anchor: .bottom) + } + // .onChange() { + // scrollReader.scrollTo(10000, anchor: .bottomTrailing) + // } + } + } + } + + Divider() + + HStack(alignment: .top) { + Image(systemName: "chevron.right") + TextField("This is a chat topic", text: $input, axis: .vertical) + .lineLimit(1...5) + .onSubmit { + // HotlineClient.shared.sendChat(message: self.input) + self.input = "" + } + }.padding() + } + } +} + +#Preview { + ChatView() +} diff --git a/Hotline/Views/HotlineView.swift b/Hotline/Views/HotlineView.swift new file mode 100644 index 0000000..67ef067 --- /dev/null +++ b/Hotline/Views/HotlineView.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct HotlineView: View { + @Environment(HotlineClient.self) private var hotline + @Environment(HotlineTrackerClient.self) private var tracker + + @State private var isTrackerVisible = false + + var body: some View { + NavigationStack { + TrackerView() + } + } +} + +#Preview { + HotlineView() + .environment(HotlineClient()) + .environment(HotlineTrackerClient(tracker: HotlineTracker("hltracker.com"))) +} diff --git a/Hotline/Views/ServerView.swift b/Hotline/Views/ServerView.swift new file mode 100644 index 0000000..1610aa7 --- /dev/null +++ b/Hotline/Views/ServerView.swift @@ -0,0 +1,62 @@ +import SwiftUI + +struct ServerView: View { + @Environment(HotlineClient.self) private var hotline + @Environment(HotlineTrackerClient.self) private var tracker + + @State private var isTrackerVisible = false + + var body: some View { + TabView { + NavigationView { + ChatView() + .navigationTitle("Badmoon") + .navigationBarItems( + leading: Button(action: { + withAnimation { + isTrackerVisible.toggle() + } + }) { + Image(systemName: "line.horizontal.3") // Hamburger icon or similar + .imageScale(.large) + } + ) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarBackground(.red, for: .navigationBar) + } + .navigationBarTitleDisplayMode(.inline) + .tabItem { + Image(systemName: "message") + } + + Text("Users") + .tabItem { + Image(systemName: "person.fill") + } + + Text("News") + .tabItem { + Image(systemName: "newspaper") + } + + Text("Files") + .tabItem { + Image(systemName: "folder") + } + + Text("Transfers") + .tabItem { + Image(systemName: "network") + } + } +// .sheet(isPresented: Binding(get: { hotline.connectionStatus != .loggedIn }, set: { _ in })) { +// TrackerView() +// } + } +} + +#Preview { + ServerView() + .environment(HotlineClient()) + .environment(HotlineTrackerClient(tracker: HotlineTracker("hltracker.com"))) +} diff --git a/Hotline/Views/TrackerServerView.swift b/Hotline/Views/TrackerServerView.swift new file mode 100644 index 0000000..5be6f7a --- /dev/null +++ b/Hotline/Views/TrackerServerView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct TrackerServerView: View { + @Environment(\.dismiss) var dismiss + + let server: HotlineServer + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text("🌎").dynamicTypeSize(.xxxLarge) + Text(server.name!).bold().dynamicTypeSize(.xxxLarge) + } + .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)).textSelection(.enabled) + Text(server.address).opacity(0.3).dynamicTypeSize(.medium).textSelection(.enabled) + + Spacer() + + HStack(alignment: .center) { + Button("Connect") { + print("WHAT", "HELLO", server.address) +// HotlineClient.shared.connect(to: server) + dismiss() + } + .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) + } +} + +#Preview { + TrackerServerView(server: HotlineServer(address: "192.168.1.1", port: 5050, users: 5, name: "Ye Olde Server", description: "This is a server")) +} diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift new file mode 100644 index 0000000..5d1afe2 --- /dev/null +++ b/Hotline/Views/TrackerView.swift @@ -0,0 +1,73 @@ +import SwiftUI + +struct TrackerView: View { + + // @Environment(\.modelContext) private var modelContext + // @Query private var items: [Item] + + @Environment(HotlineClient.self) private var hotline + @Environment(HotlineTrackerClient.self) private var tracker + + // @StateObject var tracker = HotlineTrackerClient(address: "hltracker.com") + // @StateObject var client = HotlineClient.shared + + @State private var selectedServer: HotlineServer? + @State private var showingAgreement = false + @State private var showingConnectSheet = false + + // @Bindable var trackerTest = tracker + + var body: some View { + List(selection: $selectedServer) { + ForEach(tracker.servers) { server in + HStack { + Text("🌎") + Text(server.name!).bold() + Spacer() + Text("\(server.users)").opacity(0.6) + } + .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) + } + } + .background(Color(white: 0.96)) + .listStyle(.plain) + .frame(maxWidth: .infinity) + .task { + tracker.fetch() + } + .sheet(item: $selectedServer) { item in + TrackerServerView(server: item) + } + .sheet(isPresented: Binding(get: { hotline.agreement != nil }, set: { _ in })) { + AgreementView(text: hotline.agreement!) + } + .navigationTitle("Tracker") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Image(systemName: "gearshape.fill") + } + // ToolbarItem(placement: .topBarTrailing) { + // Image(systemName: "camera.fill") + // } + // ToolbarItem(placement: .principal) { + // Text("Username") + // } + } + .refreshable { + tracker.fetch() + } + .interactiveDismissDisabled() + } +} + +#Preview { + TrackerView() + .environment(HotlineClient()) + .environment(HotlineTrackerClient(tracker: HotlineTracker("hltracker.com"))) + // .modelContainer(for: Item.self, inMemory: true) +} |