diff options
| author | Dustin Mierau <dustin@mierau.me> | 2023-12-06 13:52:20 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2023-12-06 13:52:20 -0800 |
| commit | c0068f11c51bb587c16dfacb73629b3c6fb67fc8 (patch) | |
| tree | 20753433f348b03bb590e0fe6a4cafbe56f4c859 /Hotline | |
| parent | 06a2166415bca7e5fe32eac505859bdd690ab8e0 (diff) | |
Further work on UI organization
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 85 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineProtocol.swift | 57 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineTrackerClient.swift | 4 | ||||
| -rw-r--r-- | Hotline/HotlineApp.swift | 2 | ||||
| -rw-r--r-- | Hotline/HotlineState.swift (renamed from Hotline/Views/HotlineState.swift) | 0 | ||||
| -rw-r--r-- | Hotline/Utility/DataExtensions.swift | 11 | ||||
| -rw-r--r-- | Hotline/Views/AgreementView.swift | 40 | ||||
| -rw-r--r-- | Hotline/Views/ChatView.swift | 156 | ||||
| -rw-r--r-- | Hotline/Views/FileListView.swift | 26 | ||||
| -rw-r--r-- | Hotline/Views/FilesView.swift | 88 | ||||
| -rw-r--r-- | Hotline/Views/HotlineView.swift | 9 | ||||
| -rw-r--r-- | Hotline/Views/MessageBoardView.swift | 72 | ||||
| -rw-r--r-- | Hotline/Views/NewsView.swift | 70 | ||||
| -rw-r--r-- | Hotline/Views/ServerView.swift | 126 | ||||
| -rw-r--r-- | Hotline/Views/TrackerServerView.swift | 94 | ||||
| -rw-r--r-- | Hotline/Views/TrackerView.swift | 103 | ||||
| -rw-r--r-- | Hotline/Views/UserListView.swift | 30 |
17 files changed, 549 insertions, 424 deletions
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index aaf2168..f108c79 100644 --- a/Hotline/Hotline/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -9,24 +9,32 @@ enum HotlineClientStatus: Int { case loggedIn } +enum HotlineChatType: Int { + case message + case agreement + case status +} + struct HotlineChat: Identifiable { let id = UUID() - let message: String + let text: String let username: String - let isTopic: Bool + let type: HotlineChatType static let parser = /\s+(.+):\s+(.*)/ - init(message: String, isTopic: Bool = false) { - self.isTopic = isTopic + init(text: String, type: HotlineChatType = .message) { + self.type = type - if let match = message.firstMatch(of: HotlineChat.parser) { + if + type == .message, + let match = text.firstMatch(of: HotlineChat.parser) { self.username = String(match.1) - self.message = String(match.2) + self.text = String(match.2) } else { self.username = "" - self.message = message + self.text = text } } } @@ -47,7 +55,6 @@ class HotlineClient { var users: [UInt16:HotlineUser] = [:] var userList: [HotlineUser] = [] var chatMessages: [HotlineChat] = [] - var messageBoard: String = "" var messageBoardMessages: [String] = [] var fileList: [HotlineFile] = [] @@ -60,7 +67,8 @@ class HotlineClient { @ObservationIgnored private var transactionLog: [UInt32:HotlineTransactionType] = [:] init() { - +// let downloadsPath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask) +// print("DOWNLOAD TO: \(downloadsPath)") } // MARK: - @@ -91,11 +99,13 @@ class HotlineClient { DispatchQueue.main.async { self?.connectionStatus = .disconnected } + self?.reset() case .failed(let err): print("HotlineClient: connection error \(err)") DispatchQueue.main.async { self?.connectionStatus = .disconnected } + self?.reset() default: print("HotlineClient: unhandled connection state \(newState)") } @@ -107,15 +117,22 @@ class HotlineClient { self.connection?.start(queue: .global()) } - private func disconnect() { - self.connection?.cancel() - self.connection = nil - + func reset() { DispatchQueue.main.async { - self.connectionStatus = .disconnected + self.chatMessages = [] + self.agreement = nil + self.users = [:] + self.userList = [] + self.messageBoardMessages = [] + self.fileList = [] } } + func disconnect() { + self.connection?.cancel() + self.connection = nil + } + // MARK: - private func sendTransaction(_ t: HotlineTransaction, autodisconnect disconnectOnError: Bool = true, callback: (() -> Void)? = nil) { @@ -343,16 +360,26 @@ class HotlineClient { self.sendTransaction(t, callback: callback) } - func sendGetNews(callback: (() -> Void)? = nil) { + func sendGetMessageBoard(callback: (() -> Void)? = nil) { let t = HotlineTransaction(type: .getMessages) self.sendTransaction(t, callback: callback) } + func sendGetNewsCategories(callback: (() -> Void)? = nil) { + let t = HotlineTransaction(type: .getNewsCategoryNameList) + self.sendTransaction(t, callback: callback) + } + + func sendGetNewsArticles(callback: (() -> Void)? = nil) { + let t = HotlineTransaction(type: .getNewsArticleNameList) + self.sendTransaction(t, callback: callback) + } + func sendGetFileList(path: String? = nil, callback: (() -> Void)? = nil) { let t = HotlineTransaction(type: .getFileNameList) - if let p = path { +// if let p = path { // t.setFieldString(type: .filePath) - } +// } self.sendTransaction(t, callback: callback) } @@ -407,14 +434,19 @@ class HotlineClient { let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/ let matches = text.matches(of: messageBoardRegex) var start = text.startIndex - for match in matches { - let range = match.range - messages.append(String(text[start..<range.lowerBound])) - start = range.upperBound + + if matches.count > 0 { + for match in matches { + let range = match.range + messages.append(String(text[start..<range.lowerBound])) + start = range.upperBound + } + } + else { + messages.append(text) } DispatchQueue.main.async { - self.messageBoard = text self.messageBoardMessages = messages } } @@ -422,12 +454,16 @@ class HotlineClient { var files: [HotlineFile] = [] for fi in transaction.getFieldList(type: .fileNameWithInfo) { let file = fi.getFile() -// print("GOT FILE: \(file.name) \(file.creator) \(file.type) \(file.fileSize)") files.append(file) } DispatchQueue.main.async { self.fileList = files } + case .getNewsCategoryNameList: + for fi in transaction.getFieldList(type: .newsCategoryListData15) { + let c = fi.getNewsCategory() + print("CATEGORY: \(c)") + } default: break } @@ -459,7 +495,7 @@ class HotlineClient { { print("HotlineClient: \(chatText)") DispatchQueue.main.async { - self.chatMessages.append(HotlineChat(message: chatText, isTopic: false)) + self.chatMessages.append(HotlineChat(text: chatText, type: .message)) } } case .notifyOfUserChange: @@ -481,6 +517,7 @@ class HotlineClient { print(agreementText) print("\n--------------------------\n\n") DispatchQueue.main.async { + self.chatMessages.insert(HotlineChat(text: agreementText, type: .agreement), at: 0) self.agreement = agreementText } // self.sendAgree() { diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift index a7392ec..0cf3d06 100644 --- a/Hotline/Hotline/HotlineProtocol.swift +++ b/Hotline/Hotline/HotlineProtocol.swift @@ -29,6 +29,51 @@ extension UInt32 { } } +struct HotlineNewsCategory: Identifiable, Hashable { + let id = UUID() + let type: UInt16 + let count: UInt16 + let name: String + + static func == (lhs: HotlineNewsCategory, rhs: HotlineNewsCategory) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } + + init(type: UInt16, count: UInt16, name: String) { + self.type = type + self.count = count + self.name = name + } + + init(from data: Data) { + self.type = data.readUInt16(at: 0)! + + if self.type == 2 { + // Read bundle properties + self.count = data.readUInt16(at: 2)! + + let nameSize = data.readUInt8(at: 4)! + self.name = data.readString(at: 5, length: Int(nameSize))! + } + else if self.type == 3 { + // Read category properties + self.count = data.readUInt16(at: 2)! + + let nameSize = data.readUInt8(at: 2 + 2 + 4 + 4)! + self.name = data.readString(at: 2 + 2 + 4 + 4 + 1, length: Int(nameSize))! + } + else { + self.count = 0 + self.name = "" + } + } +} + + struct HotlineFile: Identifiable, Hashable { let id = UUID() let type: String @@ -72,7 +117,7 @@ struct HotlineFile: Identifiable, Hashable { // let nameScript = data.readUInt16(at: 16)! // name script let nameLength = data.readUInt16(at: 18)! - self.name = data.readString(at: 20, length: Int(nameLength), encoding: .ascii)! + self.name = data.readString(at: 20, length: Int(nameLength))! } } @@ -107,7 +152,7 @@ struct HotlineUser: Identifiable, Hashable { self.status = data.readUInt16(at: 4)! let userNameLength = Int(data.readUInt16(at: 6)!) - self.name = data.readString(at: 8, length: userNameLength, encoding: .ascii)! + self.name = data.readString(at: 8, length: userNameLength)! } func hash(into hasher: inout Hasher) { @@ -210,8 +255,8 @@ struct HotlineTransactionField { return nil } - func getString(encoding: String.Encoding = .ascii) -> String? { - return String(data: self.data, encoding: encoding) + func getString() -> String? { + return String(data: self.data, encoding: .utf8) ?? String(data: self.data, encoding: .ascii) } func getUser() -> HotlineUser { @@ -221,6 +266,10 @@ struct HotlineTransactionField { func getFile() -> HotlineFile { return HotlineFile(from: self.data) } + + func getNewsCategory() -> HotlineNewsCategory { + return HotlineNewsCategory(from: self.data) + } } struct HotlineTransaction { diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift index cff3888..4ae252c 100644 --- a/Hotline/Hotline/HotlineTrackerClient.swift +++ b/Hotline/Hotline/HotlineTrackerClient.swift @@ -250,10 +250,10 @@ class HotlineTrackerClient { let nameLengthByte = self.bytes.readUInt8(at: cursor + 10) { let nameLength = Int(nameLengthByte) - if let name = self.bytes.readString(at: cursor + 11, length: nameLength, encoding: .utf8) ?? self.bytes.readString(at: cursor + 11, length: nameLength, encoding: .ascii) { + 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, encoding: .utf8) ?? self.bytes.readString(at: cursor + 11 + nameLength + 1, length: descLength, encoding: .ascii) { + 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)") diff --git a/Hotline/HotlineApp.swift b/Hotline/HotlineApp.swift index d999e89..3e714d8 100644 --- a/Hotline/HotlineApp.swift +++ b/Hotline/HotlineApp.swift @@ -23,7 +23,7 @@ struct HotlineApp: App { var body: some Scene { WindowGroup { - HotlineView() + TrackerView() .environment(appState) .environment(hotline) .environment(tracker) diff --git a/Hotline/Views/HotlineState.swift b/Hotline/HotlineState.swift index 9ae4940..9ae4940 100644 --- a/Hotline/Views/HotlineState.swift +++ b/Hotline/HotlineState.swift diff --git a/Hotline/Utility/DataExtensions.swift b/Hotline/Utility/DataExtensions.swift index 9f4c967..aefd204 100644 --- a/Hotline/Utility/DataExtensions.swift +++ b/Hotline/Utility/DataExtensions.swift @@ -51,8 +51,15 @@ extension Data { return self.subdata(in: offset..<(offset + length)) } - func readString(at offset: Int, length: Int, encoding: String.Encoding) -> String? { - return String(data: self[offset..<(offset + length)], encoding: encoding) + func readString(at offset: Int, length: Int) -> String? { + var str: String? + + str = String(data: self[offset..<(offset + length)], encoding: .utf8) + if str == nil { + str = String(data: self[offset..<(offset + length)], encoding: .ascii) + } + + return str } diff --git a/Hotline/Views/AgreementView.swift b/Hotline/Views/AgreementView.swift deleted file mode 100644 index 39730ce..0000000 --- a/Hotline/Views/AgreementView.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SwiftUI - -struct AgreementView: View { - @Environment(HotlineState.self) private var appState - - let text: String - - var body: some View { -// @Bindable var config = appState - - VStack(alignment: .leading) { - ScrollView { - VStack(alignment: .leading) { - Text(text) - .fontDesign(.monospaced) - .padding() - .dynamicTypeSize(.small) - .textSelection(.enabled) - } - } - Button("OK") { - print("DONE") - appState.dismissAgreement() - } - .bold() - .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) - .frame(maxWidth: .infinity) - } - .presentationDetents([.fraction(0.6)]) - .presentationDragIndicator(.visible) - } -} - -#Preview { - AgreementView(text: """ -Welcome! - -Take it on real one. -""") -} diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift index e28e076..dc6e1d4 100644 --- a/Hotline/Views/ChatView.swift +++ b/Hotline/Views/ChatView.swift @@ -7,63 +7,117 @@ struct ChatView: View { @State private var scrollPos: Int? @State private var contentHeight: CGFloat = 0 + @Namespace var bottomID + var body: some View { - VStack(spacing: 0) { - List(hotline.chatMessages) { msg in - HStack(alignment: .firstTextBaseline) { - if !msg.username.isEmpty { - Text("\(msg.username):").bold().fontDesign(.monospaced).font(.system(size: 12)) + NavigationStack { + VStack(spacing: 0) { + ScrollView { + ScrollViewReader { reader in + LazyVStack(alignment: .leading) { + ForEach(hotline.chatMessages) { msg in + if msg.type == .agreement { + + VStack(alignment: .leading) { + Text(msg.text) + .padding() + .opacity(0.75) + .background(Color(white: 0.96)) + .cornerRadius(20) + } + .padding() + } + else { + HStack(alignment: .firstTextBaseline) { + if !msg.username.isEmpty { + Text("\(msg.username):").bold() + } + Text(msg.text) + .textSelection(.enabled) + Spacer() + } + .padding() + } + } + Text("").id(bottomID) + } + .onChange(of: hotline.chatMessages.count) { + withAnimation { + reader.scrollTo(bottomID, anchor: .bottom) + } + print("SCROLLED TO BOTTOM") + } + .onAppear { + withAnimation { + reader.scrollTo("bottom view", anchor: .bottom) + } + } } - Text(msg.message) - .fontDesign(.monospaced) - .textSelection(.enabled) - .font(.system(size: 12)) } - .listRowSeparator(.hidden) + + Divider() + + HStack(alignment: .top) { + Image(systemName: "chevron.right") + TextField("", text: $input, axis: .vertical) + .lineLimit(1...5) + .onSubmit { + hotline.sendChat(message: self.input) + // HotlineClient.shared.sendChat(message: self.input) + self.input = "" + } + }.padding() } - .listStyle(.plain) - -// GeometryReader { geometry in -// ScrollView(.vertical) { -// ScrollViewReader { scrollReader in -// VStack(alignment: .leading) { -// Spacer() -// List(hotline.chatMessages) { msg in -// HStack(alignment: .firstTextBaseline) { -// Text("\(msg.username):").bold().fontDesign(.monospaced) -// Text(msg.message) -// .fontDesign(.monospaced) -// .textSelection(.enabled) -// } -// } -// .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("", text: $input, axis: .vertical) - .lineLimit(1...5) - .onSubmit { - hotline.sendChat(message: self.input) - // HotlineClient.shared.sendChat(message: self.input) - self.input = "" + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text(hotline.server?.name ?? "") + .font(.headline) + } + ToolbarItem(placement: .navigationBarLeading) { + Button { + hotline.disconnect() + } label: { + Image(systemName: "xmark.circle.fill") + .symbolRenderingMode(.hierarchical) + .foregroundColor(.secondary) } - }.padding() + + } + } + } + + + // GeometryReader { geometry in + // ScrollView(.vertical) { + // ScrollViewReader { scrollReader in + // VStack(alignment: .leading) { + // Spacer() + // List(hotline.chatMessages) { msg in + // HStack(alignment: .firstTextBaseline) { + // Text("\(msg.username):").bold().fontDesign(.monospaced) + // Text(msg.message) + // .fontDesign(.monospaced) + // .textSelection(.enabled) + // } + // } + // .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) + // // } + // } + // } + // } + + } } diff --git a/Hotline/Views/FileListView.swift b/Hotline/Views/FileListView.swift deleted file mode 100644 index a315757..0000000 --- a/Hotline/Views/FileListView.swift +++ /dev/null @@ -1,26 +0,0 @@ -import SwiftUI - -struct FileListView: View { - @State var files: [HotlineFile] - - var body: some View { - Text("HI") -// @Bindable var fls = files -// List { -// ForEach($fls, id: \.self) { f in -// if f.isFolder { -// DisclosureGroup(f.name, isExpanded: false) -// } -// else { -// Text("HELLO") -// } -// } -// } - } -} - -#Preview { - FileListView(files: [ - HotlineFile(type: "fldr", creator: "", fileSize: 0, fileName: "Folder") - ]) -} diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift index 7bacbce..1b35695 100644 --- a/Hotline/Views/FilesView.swift +++ b/Hotline/Views/FilesView.swift @@ -1,4 +1,5 @@ import SwiftUI +import UniformTypeIdentifiers struct FilesView: View { @Environment(HotlineClient.self) private var hotline @@ -14,28 +15,79 @@ struct FilesView: View { 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 { - List(hotline.fileList, id: \.self, children: \.files) { tree in - HStack { - if tree.isFolder { - Image(systemName: "folder") - Text(tree.name).bold() - Spacer() - Text("\(tree.fileSize)").foregroundStyle(.gray) + NavigationStack { + List(hotline.fileList, id: \.self, children: \.files) { tree in + HStack { + if tree.isFolder { + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Image(systemName: "folder.fill") + } + .frame(minWidth: 25) + Text(tree.name).bold() + Spacer() + Text("\(tree.fileSize)").foregroundStyle(.gray) + } + else { + HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { + Image(uiImage: fileIcon(name: tree.name)) + } + .frame(minWidth: 25) + Text(tree.name).bold() + Spacer() + Text(formattedFileSize(tree.fileSize)).foregroundStyle(.gray) + } } - else { - Image(systemName: "doc") - Text(tree.name).bold() - Spacer() - Text(formattedFileSize(tree.fileSize)).foregroundStyle(.gray) + } + .listStyle(.plain) + .task { + if !fetched { + hotline.sendGetFileList() { + fetched = true + } } } - } - .listStyle(.plain) - .task { - if !fetched { - hotline.sendGetFileList() { - fetched = true + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text(hotline.server?.name ?? "") + .font(.headline) + } + ToolbarItem(placement: .navigationBarLeading) { + Button { + hotline.disconnect() + } label: { + Image(systemName: "xmark.circle.fill") + .symbolRenderingMode(.hierarchical) + .foregroundColor(.secondary) + } + } } } diff --git a/Hotline/Views/HotlineView.swift b/Hotline/Views/HotlineView.swift index 713691f..f950dd7 100644 --- a/Hotline/Views/HotlineView.swift +++ b/Hotline/Views/HotlineView.swift @@ -6,14 +6,7 @@ struct HotlineView: View { @Environment(HotlineTrackerClient.self) private var tracker var body: some View { - @Bindable var config = appState - - NavigationStack { - TrackerView() - } - .sheet(isPresented: $config.agreementPresented) { - AgreementView(text: hotline.agreement!) - } + TrackerView() } } diff --git a/Hotline/Views/MessageBoardView.swift b/Hotline/Views/MessageBoardView.swift index 8b1a30c..35b4282 100644 --- a/Hotline/Views/MessageBoardView.swift +++ b/Hotline/Views/MessageBoardView.swift @@ -8,36 +8,58 @@ struct MessageBoardView: View { var body: some View { // @Bindable var config = appState - - ScrollView { - LazyVStack(alignment: .leading) { - ForEach(hotline.messageBoardMessages, id: \.self) { - Text($0) - .lineLimit(100) - .padding() - Divider() + NavigationStack { + ScrollView { + LazyVStack(alignment: .leading) { + ForEach(hotline.messageBoardMessages, id: \.self) { + Text($0) + .lineLimit(100) + .padding() + .textSelection(.enabled) + Divider() + } } + Spacer() } - } -// List(hotline.messageBoardMessages, id: \.self) { -// Text($0) -// .lineLimit(100) -// .padding() -// Divider() -// .listRowSeparator(.hidden) -// .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) -// } -// .listStyle(.plain) - .task { - if !fetched { - hotline.sendGetNews() { - fetched = true + .task { + if !fetched { + hotline.sendGetMessageBoard() { + fetched = true + } + } + } + .refreshable { + hotline.sendGetMessageBoard() + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text(hotline.server?.name ?? "") + .font(.headline) + } + ToolbarItem(placement: .navigationBarLeading) { + Button { + hotline.disconnect() + } label: { + Image(systemName: "xmark.circle.fill") + .symbolRenderingMode(.hierarchical) + .foregroundColor(.secondary) + } + + } + ToolbarItem(placement: .navigationBarTrailing) { + Button { + + } label: { + Image(systemName: "square.and.pencil") +// .symbolRenderingMode(.hierarchical) +// .foregroundColor(.secondary) + } + } } } - .refreshable { - hotline.sendGetNews() - } + } } diff --git a/Hotline/Views/NewsView.swift b/Hotline/Views/NewsView.swift new file mode 100644 index 0000000..5f2dbe9 --- /dev/null +++ b/Hotline/Views/NewsView.swift @@ -0,0 +1,70 @@ +import SwiftUI + +struct NewsView: View { + @Environment(HotlineState.self) private var appState + @Environment(HotlineClient.self) private var hotline + + @State private var fetched = false + + var body: some View { +// @Bindable var config = appState + NavigationStack { + ScrollView { + LazyVStack(alignment: .leading) { +// ForEach(hotline.messageBoardMessages, id: \.self) { +// Text($0) +// .lineLimit(100) +// .padding() +// .textSelection(.enabled) +// Divider() +// } + } + Spacer() + } + .task { + if !fetched { + hotline.sendGetNewsCategories() { + fetched = true + } + } + } + .refreshable { + hotline.sendGetNewsCategories() + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text(hotline.server?.name ?? "") + .font(.headline) + } + ToolbarItem(placement: .navigationBarLeading) { + Button { + hotline.disconnect() + } label: { + Image(systemName: "xmark.circle.fill") + .symbolRenderingMode(.hierarchical) + .foregroundColor(.secondary) + } + + } + ToolbarItem(placement: .navigationBarTrailing) { + Button { + + } label: { + Image(systemName: "square.and.pencil") +// .symbolRenderingMode(.hierarchical) +// .foregroundColor(.secondary) + } + + } + } + } + + } +} + +#Preview { + MessageBoardView() + .environment(HotlineState()) + .environment(HotlineClient()) +} diff --git a/Hotline/Views/ServerView.swift b/Hotline/Views/ServerView.swift index ecb13a1..8e50d83 100644 --- a/Hotline/Views/ServerView.swift +++ b/Hotline/Views/ServerView.swift @@ -1,11 +1,8 @@ import SwiftUI struct ServerView: View { - @Environment(HotlineState.self) private var appState @Environment(HotlineClient.self) private var hotline - @Environment(HotlineTrackerClient.self) private var tracker - - let server: HotlineServer + @Environment(\.colorScheme) var colorScheme func connectionStatusTitle(status: HotlineClientStatus) -> String { switch(status) { @@ -26,102 +23,47 @@ struct ServerView: View { return Double(status.rawValue) / Double(HotlineClientStatus.loggedIn.rawValue) } + enum Tab { + case chat, users, news, messageBoard, files + } + var body: some View { - @Bindable var config = appState - - NavigationStack { - if hotline.connectionStatus != .loggedIn { - VStack { - Spacer() - VStack(alignment: .center) { - Text("🌎").font(.largeTitle) - Text(server.name!).font(.title3).fontWeight(.medium) - Text(server.description!).opacity(0.6).font(.title3) - Text(server.address).opacity(0.6).font(.title3) - } - Spacer() - HStack(alignment: .center) { - if hotline.connectionStatus == .disconnected { - ProgressView(connectionStatusTitle(status: hotline.connectionStatus), value: connectionProgress(status: hotline.connectionStatus)) - Button("Connect") { - hotline.connect(to: server) - // config.dismissTracker() - } - .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) - } - else { - ProgressView("", value: Double(hotline.connectionStatus.rawValue / HotlineClientStatus.loggedIn.rawValue)) - } - } + TabView { + ChatView() + .tabItem { + Image(systemName: "message") } - .padding() - } - else { - TabView { - ChatView() - .navigationTitle(hotline.server?.name ?? "Hotline") - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems( - leading: Button(action: { - appState.presentTracker() - }) { - Image(systemName: "globe.americas.fill") // Hamburger icon or similar - .imageScale(.large) - } - ) - // .toolbarBackground(.visible, for: .navigationBar) - // .toolbarBackground(.red, for: .navigationBar) - .tabItem { - Image(systemName: "message") - } - - UserListView() - .navigationTitle("User List") - .navigationBarTitleDisplayMode(.inline) - .tabItem { - Image(systemName: "person.fill") - } - - Text("News") - .tabItem { - Image(systemName: "newspaper") - } - - MessageBoardView() - .navigationTitle("Message Board") - .navigationBarTitleDisplayMode(.inline) - .tabItem { - Image(systemName: "pin") - } - - FilesView() - .navigationTitle("Files") - .navigationBarTitleDisplayMode(.inline) - .tabItem { - Image(systemName: "folder").tint(.black) - } + .tag(Tab.chat) + + UserListView() + .tabItem { + Image(systemName: "person.fill") } - .accentColor(.black) - } + .tag(Tab.users) - // .sheet(isPresented: Binding(get: { hotline.connectionStatus != .loggedIn }, set: { _ in })) { - // TrackerView() - // } + NewsView() + .tabItem { + Image(systemName: "newspaper") + } + .tag(Tab.news) + + MessageBoardView() + .tabItem { + Image(systemName: "pin") + } + .tag(Tab.messageBoard) + + FilesView() + .tabItem { + Image(systemName: "folder").tint(.black) + } + .tag(Tab.files) } + .accentColor(colorScheme == .dark ? .white : .black) } } #Preview { - ServerView(server: HotlineServer(address: "192.168.1.1", port: 5050, users: 5, name: "Ye Olde Server", description: "This is a server")) + ServerView() .environment(HotlineClient()) - .environment(HotlineTrackerClient(tracker: HotlineTracker("hltracker.com"))) - .environment(HotlineState()) } diff --git a/Hotline/Views/TrackerServerView.swift b/Hotline/Views/TrackerServerView.swift deleted file mode 100644 index 5288496..0000000 --- a/Hotline/Views/TrackerServerView.swift +++ /dev/null @@ -1,94 +0,0 @@ -import SwiftUI - -struct TrackerServerView: View { - @Environment(HotlineState.self) private var appState - @Environment(HotlineClient.self) private var hotline - - let server: HotlineServer - - @State private var expanded = false - - func shouldDisplayDescription(server: HotlineServer) -> Bool { - guard let name = server.name, let desc = server.description else { - return false - } - - return desc.count > 0 && desc != name && !desc.contains(/^-+/) - } - - var body: some View { - @Bindable var config = appState - - VStack(alignment: .leading) { - HStack(alignment: .firstTextBaseline) { - Text("🌎").font(.title3) - VStack(alignment: .leading) { - Text(server.name!).font(.title3).fontWeight(.medium) - if shouldDisplayDescription(server: server) { - Text(server.description!).opacity(0.6).font(.title3) - } - } - } - .padding() - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowSeparator(.hidden) - .listRowBackground(Color(white: 0.96)) - - // .padding(EdgeInsets(top: 0, leading: 0, bottom: 8.0, trailing: 0)) -// ProgressView(value: 0.5) -// -// HStack(alignment: .center) { -// Button("Connect") { -// hotline.connect(to: server) -// config.dismissTracker() -// // 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) -// } - } -// label: { -// HStack(alignment: .firstTextBaseline) { -// Text("🌎").font(.title3) -// VStack(alignment: .leading) { -// Text(server.name!).font(.title3).fontWeight(.medium) -// if shouldDisplayDescription(server: server) { -// Spacer() -// Text(server.description!).opacity(0.6).font(.title3) -// } -// } -// } -// .padding() -// .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) -// .listRowSeparator(.hidden) -// .listRowBackground(Color(white: 0.96)) -// } - .popover(isPresented: $expanded) { - Text("Popover Content") - .padding() - } - .background(Color(white: 0.96)) - .padding(EdgeInsets(top: 8.0, leading: 24.0, bottom: 8.0, trailing: 24.0)) - .presentationDetents([.fraction(0.4)]) - .presentationDragIndicator(.visible) - .onTapGesture { - expanded = true -// withAnimation { -// expanded.toggle() -// } - } - } -} - -#Preview { - TrackerServerView(server: HotlineServer(address: "192.168.1.1", port: 5050, users: 5, name: "Ye Olde Server", description: "This is a server")) - .environment(HotlineClient()) - .environment(HotlineState()) -} diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift index 83a4ebf..3ed66dd 100644 --- a/Hotline/Views/TrackerView.swift +++ b/Hotline/Views/TrackerView.swift @@ -8,6 +8,7 @@ struct TrackerView: View { @Environment(HotlineState.self) private var appState @Environment(HotlineClient.self) private var hotline @Environment(HotlineTrackerClient.self) private var tracker + @Environment(\.colorScheme) var colorScheme @State private var selectedServer: HotlineServer? @@ -19,37 +20,90 @@ struct TrackerView: View { return desc.count > 0 && desc != name && !desc.contains(/^-+/) } + func connectionStatusToProgress(status: HotlineClientStatus) -> Double { + switch status { + case .disconnected: + return 0.0 + case .connecting: + return 0.1 + case .connected: + return 0.25 + case .loggingIn: + return 0.5 + case .loggedIn: + + return 1.0 + } + } + var body: some View { @Bindable var config = appState + @Bindable var client = hotline - List(selection: $selectedServer) { - ForEach(tracker.servers) { server in - NavigationLink { - ServerView(server: server) - } label: { - HStack(alignment: .firstTextBaseline) { - Text("🌎").font(.title3) - VStack(alignment: .leading) { - Text(server.name!).font(.title3).fontWeight(.medium) - if shouldDisplayDescription(server: server) { - Text(server.description!).opacity(0.6).font(.title3) + ScrollView { + LazyVStack(alignment: .leading) { + ForEach(tracker.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) + if shouldDisplayDescription(server: server) { + Spacer() + Text(server.description!).opacity(0.4).font(.system(size: 16)) + } + Spacer() + Text("\(server.address)").opacity(0.2).font(.system(size: 13)) + } + Spacer() + Text("\(server.users)").opacity(0.2).font(.system(size: 16)).fontWeight(.medium) + } + if server == selectedServer { + Spacer(minLength: 16) + + if hotline.server == server && hotline.connectionStatus != .disconnected { + ProgressView("", value: connectionStatusToProgress(status: hotline.connectionStatus)) + } + else { + Button("Connect") { + hotline.connect(to: server) + } + .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) } } } + .multilineTextAlignment(.leading) + .padding() + .background(colorScheme == .dark ? Color(white: 0.1) : .white) + .cornerRadius(20) + .shadow(color: Color(white: 0.0, opacity: 0.1), radius: 16, x: 0, y: 10) + .onTapGesture { + withAnimation(.bouncy(duration: 0.25, extraBounce: 0.2)) { + selectedServer = server + } + } } + .padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16)) } } - .background(Color.white) - .listStyle(.plain) - .listRowSpacing(1) + .fullScreenCover(isPresented: Binding(get: { return hotline.connectionStatus == .loggedIn }, set: { _ in }), onDismiss: { + hotline.disconnect() + }) { + ServerView() + } + .background(colorScheme == .dark ? .black : Color(white: 0.95)) .frame(maxWidth: .infinity) .task { tracker.fetch() } - // .sheet(item: $selectedServer) { item in - //// Text("HELLO") - // TrackerServerView(server: item) - // } .refreshable { await withCheckedContinuation { continuation in tracker.fetch() { @@ -59,19 +113,6 @@ struct TrackerView: View { } .navigationTitle("Tracker") .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button { - config.dismissTracker() - } label: { - Image(systemName: "xmark.circle.fill") - .symbolRenderingMode(.hierarchical) - .foregroundColor(.gray) - } - } - } - - // .interactiveDismissDisabled() } } diff --git a/Hotline/Views/UserListView.swift b/Hotline/Views/UserListView.swift index 70b58b9..cae1d0c 100644 --- a/Hotline/Views/UserListView.swift +++ b/Hotline/Views/UserListView.swift @@ -4,14 +4,32 @@ struct UserListView: View { @Environment(HotlineClient.self) private var hotline var body: some View { - VStack(spacing: 0) { - List(hotline.userList) { u in - HStack(alignment: .firstTextBaseline) { - Text(u.name).bold().foregroundStyle(u.isAdmin ? Color.red : Color.black).opacity(u.isIdle ? 0.5 : 1.0) + NavigationStack { + VStack(spacing: 0) { + List(hotline.userList) { u in + HStack(alignment: .firstTextBaseline) { + Text(u.name).bold().foregroundStyle(u.isAdmin ? Color.red : Color.black).opacity(u.isIdle ? 0.5 : 1.0) + } + } + .listStyle(.plain) + .padding() + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text(hotline.server?.name ?? "") + .font(.headline) + } + ToolbarItem(placement: .navigationBarLeading) { + Button { + hotline.disconnect() + } label: { + Image(systemName: "xmark.circle.fill") + .symbolRenderingMode(.hierarchical) + .foregroundColor(.secondary) + } } } - .listStyle(.plain) - .padding() } } } |