diff options
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png | bin | 8190 -> 0 bytes | |||
| -rw-r--r-- | Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json | 2 | ||||
| -rw-r--r-- | Hotline/Assets.xcassets/AppIcon.appiconset/hotline app icon.png | bin | 0 -> 27684 bytes | |||
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 64 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineProtocol.swift | 117 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineTrackerClient.swift | 2 | ||||
| -rw-r--r-- | Hotline/Models/ChatMessage.swift | 2 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 206 | ||||
| -rw-r--r-- | Hotline/Models/NewsCategory.swift | 34 | ||||
| -rw-r--r-- | Hotline/Models/NewsInfo.swift | 58 | ||||
| -rw-r--r-- | Hotline/Models/Server.swift | 4 | ||||
| -rw-r--r-- | Hotline/Views/ChatView.swift | 5 | ||||
| -rw-r--r-- | Hotline/Views/FilesView.swift | 2 | ||||
| -rw-r--r-- | Hotline/Views/MessageBoardView.swift | 2 | ||||
| -rw-r--r-- | Hotline/Views/NewsView.swift | 188 | ||||
| -rw-r--r-- | Hotline/Views/TrackerView.swift | 8 | ||||
| -rw-r--r-- | Hotline/Views/UsersView.swift | 2 |
17 files changed, 550 insertions, 146 deletions
diff --git a/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png b/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png Binary files differdeleted file mode 100644 index c81665b..0000000 --- a/Hotline/Assets.xcassets/AppIcon.appiconset/App Icon.png +++ /dev/null diff --git a/Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json b/Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json index 1e3ebc4..fbb9876 100644 --- a/Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Hotline/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "App Icon.png", + "filename" : "hotline app icon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/Hotline/Assets.xcassets/AppIcon.appiconset/hotline app icon.png b/Hotline/Assets.xcassets/AppIcon.appiconset/hotline app icon.png Binary files differnew file mode 100644 index 0000000..57ab755 --- /dev/null +++ b/Hotline/Assets.xcassets/AppIcon.appiconset/hotline app icon.png diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index a2621b2..533a5fb 100644 --- a/Hotline/Hotline/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -550,12 +550,17 @@ class HotlineClient { } } - func sendGetNewsCategories(sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsCategory]) -> Void)?) { - let t = HotlineTransaction(type: .getNewsCategoryNameList) + func sendGetNewsCategories(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsCategory]) -> Void)?) { + var t = HotlineTransaction(type: .getNewsCategoryNameList) + if !path.isEmpty { + t.setFieldPath(type: .newsPath, val: path) + } + self.sendTransaction(t, sent: sent, reply: { rt, err in var categories: [HotlineNewsCategory] = [] for categoryListItem in rt.getFieldList(type: .newsCategoryListData15) { - let c = categoryListItem.getNewsCategory() + var c = categoryListItem.getNewsCategory() + c.path = path + [c.name] categories.append(c) print("CATEGORY: \(c)") } @@ -565,12 +570,57 @@ class HotlineClient { }) } - func sendGetNewsArticles(path: [String]? = nil, sent: ((Bool) -> Void)? = nil, reply: ((String) -> Void)? = nil) { + func sendGetNewsArticle(id articleID: UInt32, path: [String], flavor: String, sent: ((Bool) -> Void)? = nil, reply: ((String?) -> Void)? = nil) { + var t = HotlineTransaction(type: .getNewsArticleData) + t.setFieldPath(type: .newsPath, val: path) + t.setFieldUInt32(type: .newsArticleID, val: articleID) + t.setFieldString(type: .newsArticleDataFlavor, val: flavor, encoding: .ascii) + + self.sendTransaction(t, sent: sent, reply: { r, err in + if err != nil { + reply?(nil) + return + } + + let articleData = r.getField(type: .newsArticleData) + let articleString = articleData?.getString() + + print("GOT ARTICLE", articleString) + + reply?(articleString) + }) + } + + func sendGetNewsArticles(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsArticle]) -> Void)? = nil) { var t = HotlineTransaction(type: .getNewsArticleNameList) - if path != nil { - t.setFieldPath(type: .newsPath, val: path!) + if !path.isEmpty { + t.setFieldPath(type: .newsPath, val: path) } - self.sendTransaction(t, sent: sent) + self.sendTransaction(t, sent: sent, reply: { r, err in + if err != nil { + reply?([]) + return + } + + var articles: [HotlineNewsArticle] = [] + var articleData = r.getField(type: .newsArticleListData) + + print("ARTICLE DATA?", articleData) + + if let newsList = articleData?.getNewsList() { + for art in newsList.articles { + var blah = art + blah.path = path + articles.append(blah) + + print(blah.title) + } + } + + DispatchQueue.main.async { + reply?(articles) + } + }) } func sendGetFileList(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineFile]) -> Void)? = nil) { diff --git a/Hotline/Hotline/HotlineProtocol.swift b/Hotline/Hotline/HotlineProtocol.swift index 125ec98..f155c0e 100644 --- a/Hotline/Hotline/HotlineProtocol.swift +++ b/Hotline/Hotline/HotlineProtocol.swift @@ -63,32 +63,102 @@ struct HotlineServer: Identifiable, Hashable { } } -enum HotlineChatType: Int { - case message - case agreement - case status +struct HotlineNewsArticle: Identifiable { + let id: UInt32 + let parentID: UInt32 + let flags: UInt32 + let title: String + let username: String + var flavors: [(String, UInt16)] = [] + var path: [String] = [] + + static func == (lhs: HotlineNewsArticle, rhs: HotlineNewsArticle) -> Bool { + return lhs.id == rhs.id + } } -struct HotlineChat: Identifiable { - let id = UUID() - let text: String - let username: String - let type: HotlineChatType +struct HotlineNewsList: Identifiable { + let id: UInt32 + let name: String + let description: String + let count: UInt32 + var path: [String] = [] - static let parser = /^\s*([^\:]+)\:\s*(.*)/ + var articles: [HotlineNewsArticle] = [] - init(text: String, type: HotlineChatType = .message) { - self.type = type + init(from data: Data) { + self.id = data.readUInt32(at: 0)! - if - type == .message, - let match = text.firstMatch(of: HotlineChat.parser) { - self.username = String(match.1) - self.text = String(match.2) - } - else { - self.username = "" - self.text = text + self.count = data.readUInt32(at: 4)! + + let (n, nl) = data.readPString(at: 8) + self.name = n! + + let (d, dl) = data.readPString(at: 8 + nl) + self.description = d! + + var baseIndex = Int(8 + nl + dl) + + for i in 0..<Int(self.count) { + let articleID = data.readUInt32(at: baseIndex)! + baseIndex += 4 + + let timestampData = data.readData(at: baseIndex, length: 8)! + baseIndex += 8 + + let parentID = data.readUInt32(at: baseIndex)! + baseIndex += 4 + + let flags = data.readUInt32(at: baseIndex)! + baseIndex += 4 + + let flavorCount = data.readUInt16(at: baseIndex)! + baseIndex += 2 + + let titleLength = data.readUInt8(at: baseIndex)! + baseIndex += 1 + + let title = data.readString(at: baseIndex, length: Int(titleLength))! + baseIndex += Int(titleLength) + + let posterLength = data.readUInt8(at: baseIndex)! + baseIndex += 1 + + let poster = data.readString(at: baseIndex, length: Int(posterLength))! + baseIndex += Int(posterLength) + + var newArticle = HotlineNewsArticle(id: articleID, parentID: parentID, flags: flags, title: title, username: poster) + + print("ARTICLE ID: \(articleID)") + print("PARENT ID: \(parentID)") + print("FLAGS: \(flags)") + print("FLAVOR COUNT: \(flavorCount)") + print("TITLE: \(title)") + print("POSTER: \(poster)") + + for _ in 0..<Int(flavorCount) { + let flavorLength = data.readUInt8(at: baseIndex)! + baseIndex += 1 + + let flavorText = data.readString(at: baseIndex, length: Int(flavorLength))! + baseIndex += Int(flavorLength) + + let articleSize = data.readUInt16(at: baseIndex)! + baseIndex += 2 + + newArticle.flavors.append((flavorText, articleSize)) + + +// let articleString = data.readString(at: baseIndex + i + 4 + 8 + 4 + 4 + 2 + 1 + Int(titleLength) + 1 + Int(posterLength) + 1 + Int(flavorLength) + 2, length: Int(articleSize))! + + print("FLAVOR: \(flavorText)") + print("ARTICLE SIZE: \(articleSize)") +// print("ARTICLE: \(articleString)") + } + + self.articles.append(newArticle) + +// break } } } @@ -98,6 +168,7 @@ struct HotlineNewsCategory: Identifiable, Hashable { let type: UInt16 let count: UInt16 let name: String + var path: [String] = [] static func == (lhs: HotlineNewsCategory, rhs: HotlineNewsCategory) -> Bool { return lhs.id == rhs.id @@ -384,6 +455,10 @@ struct HotlineTransactionField { func getNewsCategory() -> HotlineNewsCategory { return HotlineNewsCategory(from: self.data) } + + func getNewsList() -> HotlineNewsList { + return HotlineNewsList(from: self.data) + } } struct HotlineTransaction { diff --git a/Hotline/Hotline/HotlineTrackerClient.swift b/Hotline/Hotline/HotlineTrackerClient.swift index ed1d154..6c5f01d 100644 --- a/Hotline/Hotline/HotlineTrackerClient.swift +++ b/Hotline/Hotline/HotlineTrackerClient.swift @@ -239,7 +239,6 @@ class HotlineTrackerClient { let ip_4 = self.bytes.readUInt8(at: cursor + 3), let port = self.bytes.readUInt16(at: cursor + 4), let userCount = self.bytes.readUInt16(at: cursor + 6) { -// let nameLengthByte = self.bytes.readUInt8(at: cursor + 10) { let (serverName, nameByteCount) = self.bytes.readPString(at: cursor + 10) let (serverDescription, descByteCount) = self.bytes.readPString(at: cursor + 10 + nameByteCount) @@ -248,7 +247,6 @@ class HotlineTrackerClient { let desc = serverDescription { 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) } diff --git a/Hotline/Models/ChatMessage.swift b/Hotline/Models/ChatMessage.swift index 8a57d61..e585cdf 100644 --- a/Hotline/Models/ChatMessage.swift +++ b/Hotline/Models/ChatMessage.swift @@ -15,7 +15,7 @@ struct ChatMessage: Identifiable { let date: Date let username: String? - static let parser = /^\s*([^\:]+)\:\s*(.+)/ + static let parser = /^\s*([^\:]+):\s*([\s\S]+)$/ init(text: String, type: ChatMessageType, date: Date) { self.type = type diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index 92366fa..c7474b2 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -6,17 +6,31 @@ import SwiftUI var status: HotlineClientStatus = .disconnected - var server: Server? = nil - var serverVersion: UInt16? = nil - var serverName: String? = nil + var server: Server? { + didSet { + self.updateServerTitle() + } + } + var serverVersion: UInt16? { + didSet { + self.updateServerTitle() + } + } + var serverName: String? { + didSet { + self.updateServerTitle() + } + } + var serverTitle: String = "Server" var username: String = "bolt" var iconID: UInt = 128 + var access: HotlineUserAccessOptions? var users: [User] = [] var chat: [ChatMessage] = [] var messageBoard: [String] = [] var files: [FileInfo] = [] - var news: [NewsCategory] = [] + var news: [NewsInfo] = [] // MARK: - @@ -28,8 +42,8 @@ import SwiftUI // MARK: - - @MainActor func getServers(address: String, port: Int = Tracker.defaultPort) async -> [Server] { - let fetchedServers: [HotlineServer] = await self.trackerClient.fetchServers(address: address, port: port) + @MainActor func getServerList(tracker: String, port: Int = Tracker.defaultPort) async -> [Server] { + let fetchedServers: [HotlineServer] = await self.trackerClient.fetchServers(address: tracker, port: port) var servers: [Server] = [] @@ -48,6 +62,7 @@ import SwiftUI @MainActor func login(server: Server, login: String, password: String, username: String, iconID: UInt) async -> Bool { self.server = server + self.serverName = server.name self.username = username self.iconID = iconID @@ -107,24 +122,164 @@ import SwiftUI } } - @MainActor func getNewsCategories() async -> [NewsCategory] { + @MainActor func getNewsArticle(id articleID: UInt, at path: [String], flavor: String) async -> String? { + return await withCheckedContinuation { [weak self] continuation in + self?.client.sendGetNewsArticle(id: UInt32(articleID), path: path, flavor: flavor, sent: { success in + if !success { + continuation.resume(returning: nil) + return + } + + print("GET NEWS CATS FROM \(path)") + }, reply: { articleText in +// let parentNews = self?.findNews(in: self?.news ?? [], at: path) + +// var newCategories: [NewsInfo] = [] +// for category in categories { +// newCategories.append(NewsInfo(hotlineNewsCategory: category)) +// } +// +// if let parent = existingNewsItem { +// parent.children = newCategories +// } +// else if path.isEmpty { +// self?.news = newCategories +// } + + continuation.resume(returning: articleText) + }) + } + + } + + @MainActor func getNewsList(at path: [String] = []) async -> [NewsInfo] { return await withCheckedContinuation { [weak self] continuation in - self?.client.sendGetNewsCategories(sent: { success in + var requestCategories = true + + let existingNewsItem = self?.findNews(in: self?.news ?? [], at: path) + + if existingNewsItem != nil { + if existingNewsItem!.type != .bundle { + requestCategories = false + } + } + + if requestCategories { + self?.client.sendGetNewsCategories(path: path, sent: { success in + if !success { + continuation.resume(returning: []) + return + } + + print("GET NEWS CATS FROM \(path)") + }, reply: { [weak self] categories in +// let parentNews = self?.findNews(in: self?.news ?? [], at: path) + + var newCategories: [NewsInfo] = [] + for category in categories { + newCategories.append(NewsInfo(hotlineNewsCategory: category)) + } + + if let parent = existingNewsItem { + parent.children = newCategories + } + else if path.isEmpty { + self?.news = newCategories + } + + continuation.resume(returning: newCategories) + }) + } + else { + self?.client.sendGetNewsArticles(path: path, sent: { success in + if !success { + continuation.resume(returning: []) + return + } + + print("GET NEWS ARTICLES FROM \(path)") + }, reply: { [weak self] articles in +// let parentNews = self?.findNews(in: self?.news ?? [], at: path) + print("GENERATING NEWS") + + var newArticles: [NewsInfo] = [] + for article in articles { + newArticles.append(NewsInfo(hotlineNewsArticle: article)) + } + + if let parent = existingNewsItem { + print("UNDER PARENT:", parent.name) + parent.children = newArticles + + print(parent.children) + } + else if path.isEmpty { + self?.news = newArticles + } + + continuation.resume(returning: newArticles) + }) + } + } + } + + @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] { + return await withCheckedContinuation { [weak self] continuation in + self?.client.sendGetNewsCategories(path: path, sent: { success in if !success { continuation.resume(returning: []) return } + + print("GET NEWS CATS FROM \(path)") }, reply: { [weak self] categories in - var newCategories: [NewsCategory] = [] + let parentNews = self?.findNews(in: self?.news ?? [], at: path) + + var newCategories: [NewsInfo] = [] for category in categories { - newCategories.append(NewsCategory(hotlineNewsCategory: category)) + newCategories.append(NewsInfo(hotlineNewsCategory: category)) + } + + if let parent = parentNews { + parent.children = newCategories + } + else if path.isEmpty { + self?.news = newCategories } - self?.news = newCategories continuation.resume(returning: newCategories) }) } } + + @MainActor func getArticles(at path: [String]) async -> [NewsInfo] { + return await withCheckedContinuation { [weak self] continuation in + self?.client.sendGetNewsArticles(path: path, sent: { success in + if !success { + continuation.resume(returning: []) + return + } + }, reply: { [weak self] articles in + print("ARTICLES?", articles) + + continuation.resume(returning: []) + }) +// self?.client.sendGetNewsCategories(sent: { success in +// if !success { +// continuation.resume(returning: []) +// return +// } +// }, reply: { [weak self] categories in +// var newCategories: [NewsCategory] = [] +// for category in categories { +// newCategories.append(NewsCategory(hotlineNewsCategory: category)) +// } +// self?.news = newCategories +// +// continuation.resume(returning: newCategories) +// }) + } + } // @MainActor func updateUsers() async -> [User] { @@ -143,6 +298,7 @@ import SwiftUI if status == .disconnected { self.serverVersion = nil self.serverName = nil + self.access = nil self.users = [] self.chat = [] self.messageBoard = [] @@ -210,6 +366,8 @@ import SwiftUI func hotlineReceivedUserAccess(options: HotlineUserAccessOptions) { print("Hotline: got access options") print(options, options.contains(.canSendChat), options.contains(.canBroadcast)) + + self.access = options } func hotlineReceivedError(message: String) { @@ -218,6 +376,10 @@ import SwiftUI // MARK: - Utilities + func updateServerTitle() { + self.serverTitle = self.serverName ?? self.server?.name ?? server?.address ?? "Server" + } + private func addOrUpdateHotlineUser(_ user: HotlineUser) { if let i = self.users.firstIndex(where: { $0.id == user.id }) { print("Hotline: updating user \(self.users[i].name)") @@ -233,8 +395,6 @@ import SwiftUI 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 { @@ -251,4 +411,24 @@ import SwiftUI return nil } + + private func findNews(in newsToSearch: [NewsInfo], at path: [String]) -> NewsInfo? { + guard !path.isEmpty, !newsToSearch.isEmpty else { return nil } + + let currentName = path[0] + + for news in newsToSearch { + if news.name == currentName { + if path.count == 1 { + return news + } + else if !news.children.isEmpty { + let remainingPath = Array(path[1...]) + return self.findNews(in: news.children, at: remainingPath) + } + } + } + + return nil + } } diff --git a/Hotline/Models/NewsCategory.swift b/Hotline/Models/NewsCategory.swift deleted file mode 100644 index 0ba87a7..0000000 --- a/Hotline/Models/NewsCategory.swift +++ /dev/null @@ -1,34 +0,0 @@ -import SwiftUI - -enum NewsCategoryType { - case bundle - case category -} - -@Observable class NewsCategory: Identifiable, Hashable { - let id: UUID = UUID() - - let name: String - let count: UInt16 - let type: NewsCategoryType - - init(hotlineNewsCategory: HotlineNewsCategory) { - self.name = hotlineNewsCategory.name - self.count = hotlineNewsCategory.count - - if hotlineNewsCategory.type == 2 { - self.type = .bundle - } - else { - self.type = .category - } - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } - - static func == (lhs: NewsCategory, rhs: NewsCategory) -> Bool { - return lhs.id == rhs.id - } -} diff --git a/Hotline/Models/NewsInfo.swift b/Hotline/Models/NewsInfo.swift new file mode 100644 index 0000000..ad6bfcd --- /dev/null +++ b/Hotline/Models/NewsInfo.swift @@ -0,0 +1,58 @@ +import SwiftUI + +enum NewsInfoType { + case bundle + case category + case article +} + +@Observable class NewsInfo: Identifiable, Hashable { + let id: UUID = UUID() + + let name: String + let count: UInt + let type: NewsInfoType + + let categoryID: UUID? + let articleID: UInt? + + let path: [String] + var children: [NewsInfo] = [] + + var articleFlavors: [String]? + + init(hotlineNewsCategory: HotlineNewsCategory) { + self.categoryID = hotlineNewsCategory.id + self.articleID = nil + self.name = hotlineNewsCategory.name + self.count = UInt(hotlineNewsCategory.count) + self.path = hotlineNewsCategory.path + + if hotlineNewsCategory.type == 2 { + self.type = .bundle + } + else { + self.type = .category + } + } + + init(hotlineNewsArticle: HotlineNewsArticle) { + self.articleID = UInt(hotlineNewsArticle.id) + self.categoryID = nil + self.name = hotlineNewsArticle.title + self.count = 0 +// self.count = UInt(hotlineNewsArticle.count) + self.path = hotlineNewsArticle.path + self.type = .article + + self.articleFlavors = hotlineNewsArticle.flavors.map { $0.0 } + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } + + static func == (lhs: NewsInfo, rhs: NewsInfo) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/Hotline/Models/Server.swift b/Hotline/Models/Server.swift index b159a1a..8b5ea56 100644 --- a/Hotline/Models/Server.swift +++ b/Hotline/Models/Server.swift @@ -4,13 +4,13 @@ import SwiftUI static let defaultPort: Int = 5500 let id: UUID = UUID() - let name: String + let name: String? let description: String? let users: Int let address: String let port: Int - init(name: String, description: String?, address: String, port: Int, users: Int = 0) { + init(name: String?, description: String?, address: String, port: Int, users: Int = 0) { self.name = name self.description = description self.address = address diff --git a/Hotline/Views/ChatView.swift b/Hotline/Views/ChatView.swift index 8e5b135..e8c7754 100644 --- a/Hotline/Views/ChatView.swift +++ b/Hotline/Views/ChatView.swift @@ -32,7 +32,7 @@ struct ChatView: View { .opacity(0.75) HStack { Spacer() - Text((model.server?.name ?? "") + " Server Agreement") + Text((model.serverTitle) + " Server Agreement") .font(.caption) .fontWeight(.medium) .opacity(0.4) @@ -78,6 +78,7 @@ struct ChatView: View { } .padding(.bottom, 12) } + .defaultScrollAnchor(.bottom) .onChange(of: model.chat.count) { withAnimation { reader.scrollTo(bottomID, anchor: .bottom) @@ -127,7 +128,7 @@ struct ChatView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { - Text(model.server?.name ?? "") + Text(model.serverTitle) .font(.headline) } ToolbarItem(placement: .navigationBarLeading) { diff --git a/Hotline/Views/FilesView.swift b/Hotline/Views/FilesView.swift index 5cbf0c9..8ad3837 100644 --- a/Hotline/Views/FilesView.swift +++ b/Hotline/Views/FilesView.swift @@ -116,7 +116,7 @@ struct FilesView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { - Text(model.server?.name ?? "") + Text(model.serverTitle) .font(.headline) } ToolbarItem(placement: .navigationBarLeading) { diff --git a/Hotline/Views/MessageBoardView.swift b/Hotline/Views/MessageBoardView.swift index 8a1c831..0d3968f 100644 --- a/Hotline/Views/MessageBoardView.swift +++ b/Hotline/Views/MessageBoardView.swift @@ -42,7 +42,7 @@ struct MessageBoardView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { - Text(model.server?.name ?? "") + Text(model.serverTitle) .font(.headline) } ToolbarItem(placement: .navigationBarLeading) { diff --git a/Hotline/Views/NewsView.swift b/Hotline/Views/NewsView.swift index ad7904e..bd3ae1c 100644 --- a/Hotline/Views/NewsView.swift +++ b/Hotline/Views/NewsView.swift @@ -1,58 +1,142 @@ import SwiftUI +import UniformTypeIdentifiers -struct NewsView: View { + + +struct NewsItemView: View { @Environment(Hotline.self) private var model: Hotline - @Environment(\.colorScheme) var colorScheme + @Environment(NewsItemSelection.self) private var selectedArticle: NewsItemSelection - @State private var fetched = false - @State private var selectedCategory: NewsCategory? = nil - @State private var topListHeight: CGFloat = 200 - @State private var dividerHeight: CGFloat = 30 + let news: NewsInfo - var articleList: some View { - - // Your list content goes here - List { - ForEach(model.news, id: \.self) { category in - DisclosureGroup { - ProgressView(value: 0.4) - .task { - print("EXPANDED?", category.name) -// hotline.sendGetNewsArticles(path: [cat.name]) { -// print("OK") -// } - } - } label: { - Text(category.name) + @State var expanded = false + + var body: some View { + if news.count > 0 { + DisclosureGroup(isExpanded: $expanded) { + ForEach(news.children) { childNews in + NewsItemView(news: childNews) + .environment(self.selectedArticle) + .frame(height: 38) + } + } label: { + HStack { + if news.type == .bundle { + Text(Image(systemName: "tray.2.fill")) + } + else { + Text(Image(systemName: "tray.full.fill")) + } + Text(news.name) .fontWeight(.medium) .lineLimit(1) .truncationMode(.tail) + Spacer() + if news.count > 0 { + Text("\(news.count)") + .foregroundStyle(.secondary) + } + } + } + .onChange(of: expanded) { + if !expanded { + return + } + + Task { + await model.getNewsList(at: news.path) } } } - .scrollBounceBehavior(.basedOnSize) -// .listStyle(.plain) - } - - var readerView: some View { - // Your list content goes here - ScrollView(.vertical) { - HStack(alignment: .top, spacing: 0) { - Text("HELLO") - .multilineTextAlignment(.leading) + else { + HStack { + Text(Image(systemName: "doc.text")) + Text(news.name) + .fontWeight(.medium) + .lineLimit(1) + .truncationMode(.tail) Spacer() + if news.count > 0 { + Text("\(news.count)") + .foregroundStyle(.secondary) + } + } + .onTapGesture { + if news.type == .article { + print("SELECTED", news.name) + selectedArticle.selectedArticle = news + } } - .padding() } - .scrollBounceBehavior(.basedOnSize) - .background(colorScheme == .dark ? Color(white: 0.1) : Color(uiColor: UIColor.systemBackground)) } +} + +@Observable +class NewsItemSelection: Equatable { + var selectedArticle: NewsInfo? = nil + static func == (lhs: NewsItemSelection, rhs: NewsItemSelection) -> Bool { + return lhs.selectedArticle == rhs.selectedArticle + } +} + +struct NewsView: View { + @Environment(Hotline.self) private var model: Hotline + @Environment(\.colorScheme) var colorScheme + + @State private var fetched = false + @State private var selectedCategory: NewsInfo? = nil + @State private var topListHeight: CGFloat = 200 + @State private var dividerHeight: CGFloat = 30 + + @State private var articleSelection = NewsItemSelection() + @State private var articleText = "" +// @State private var selectedArticleID: UInt? + + var articleList: some View { + VStack(spacing: 0) { + if model.news.count == 0 { + Text("No News Available") + .font(.headline) + .opacity(0.3) + } + else { + List(model.news) { category in + NewsItemView(news: category) + .environment(self.articleSelection) + .frame(height: 38) + } + .scrollBounceBehavior(.basedOnSize) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(uiColor: .systemGroupedBackground)) + + // .listStyle(.plain) + } + var body: some View { NavigationStack { VStack(spacing: 0) { articleList .frame(height: topListHeight) + .frame(minHeight: topListHeight) + .onChange(of: self.articleSelection.selectedArticle) { + self.articleText = "" + if + let article = self.articleSelection.selectedArticle, + let articleFlavor = article.articleFlavors?.first, + let articleID = article.articleID { + Task { + if let articleText = await self.model.getNewsArticle(id: articleID, at: article.path, flavor: articleFlavor) { + self.articleText = articleText + } + } + } +// print("SELECTED ARTICLE", articleSelection.selectedArticle?.name) + } + + // Movable Divider VStack(alignment: .center) { Divider() Spacer() @@ -72,28 +156,31 @@ struct NewsView: View { .onChanged { gesture in let delta = gesture.translation.height topListHeight = max(min(topListHeight + delta, 500), 50) - // bottomListHeight = max(min(bottomListHeight - delta, 400), 0) } ) - readerView + + // Reader View + ScrollView(.vertical) { + HStack(alignment: .top, spacing: 0) { + Text(self.articleText) + .multilineTextAlignment(.leading) + Spacer() + } + .padding() + } + .scrollBounceBehavior(.basedOnSize) + .background(colorScheme == .dark ? Color(white: 0.1) : Color(uiColor: UIColor.systemBackground)) } .task { if !fetched { - let _ = await model.getNewsCategories() + let _ = await model.getNewsList() fetched = true - - // hotline.sendGetNewsArticles(path: ["News"]) { - // print("GOT ARTICLES?") - // } } } - // .refreshable { - // hotline.sendGetNewsCategories() - // } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { - Text(model.server?.name ?? "") + Text(model.serverTitle) .font(.headline) } ToolbarItem(placement: .navigationBarLeading) { @@ -106,19 +193,8 @@ struct NewsView: View { .foregroundColor(.secondary) } } -// ToolbarItem(placement: .navigationBarTrailing) { -// Button { -// -// } label: { -// Image(systemName: "square.and.pencil") -// // .symbolRenderingMode(.hierarchical) -// // .foregroundColor(.secondary) -// } -// -// } } } - } } diff --git a/Hotline/Views/TrackerView.swift b/Hotline/Views/TrackerView.swift index acb2069..6204fd2 100644 --- a/Hotline/Views/TrackerView.swift +++ b/Hotline/Views/TrackerView.swift @@ -86,7 +86,7 @@ struct TrackerConnectView: View { ) .cornerRadius(10.0) Button { - let s = Server(name: address, description: nil, address: address, port: Server.defaultPort, users: 0) + let s = Server(name: nil, description: nil, address: address, port: Server.defaultPort, users: 0) server = s connecting = true Task { @@ -200,7 +200,7 @@ struct TrackerView: View { // "tracked.nailbat.com" // "hotline.duckdns.org" // "tracked.agent79.org" - self.servers = await model.getServers(address: "hltracker.com") + self.servers = await model.getServerList(tracker: "hltracker.com") } var body: some View { @@ -267,7 +267,7 @@ struct TrackerView: View { HStack(alignment: .firstTextBaseline) { Image(systemName: "globe.americas.fill").font(.title3) VStack(alignment: .leading) { - Text(server.name).font(.title3).fontWeight(.medium) + Text(server.name ?? server.address).font(.title3).fontWeight(.medium) if shouldDisplayDescription(server: server) { Spacer() Text(server.description!).opacity(0.5).font(.system(size: 16)) @@ -382,7 +382,7 @@ struct TrackerView: View { Task { model.disconnect() - let _ = await model.login(server: Server(name: address, description: nil, address: address, port: port, users: 0), login: login, password: password, username: "bolt", iconID: 128) + let _ = await model.login(server: Server(name: nil, description: nil, address: address, port: port, users: 0), login: login, password: password, username: "bolt", iconID: 128) } // TODO: Find a better way to show login status when trying to connect outside of the Tracker server list. Perhaps this opens the connect sheet prefilled. diff --git a/Hotline/Views/UsersView.swift b/Hotline/Views/UsersView.swift index f923ff4..d1f3318 100644 --- a/Hotline/Views/UsersView.swift +++ b/Hotline/Views/UsersView.swift @@ -17,7 +17,7 @@ struct UsersView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { - Text(model.server?.name ?? "") + Text(model.serverTitle) .font(.headline) } ToolbarItem(placement: .navigationBarLeading) { |