diff options
| author | Dustin Mierau <dustin@mierau.me> | 2024-01-09 10:30:44 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2024-01-09 10:30:44 -0800 |
| commit | 382121de2e79303845331d699adcdd777f8f062a (patch) | |
| tree | 83bab877069d3a0daddcb6c2a031400b99c60756 /Hotline | |
| parent | 0910d3380755d38a93e5b87a31ecf31f17d0bc58 (diff) | |
Split view in News saves position and hiding when an article isn't selected. Some code cleanup. Some plumbing for news posting though it's not hooked up yet.
Diffstat (limited to 'Hotline')
| -rw-r--r-- | Hotline/Application.swift | 10 | ||||
| -rw-r--r-- | Hotline/Hotline/HotlineClient.swift | 116 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 2 | ||||
| -rw-r--r-- | Hotline/Models/NewsArticle.swift | 21 | ||||
| -rw-r--r-- | Hotline/Shared/URLAdditions.swift | 16 | ||||
| -rw-r--r-- | Hotline/macOS/FilePreviewTextView.swift | 2 | ||||
| -rw-r--r-- | Hotline/macOS/NewsEditorView.swift | 100 | ||||
| -rw-r--r-- | Hotline/macOS/NewsView.swift | 358 | ||||
| -rw-r--r-- | Hotline/macOS/TrackerView.swift | 2 |
9 files changed, 381 insertions, 246 deletions
diff --git a/Hotline/Application.swift b/Hotline/Application.swift index 4541ea3..e7445dc 100644 --- a/Hotline/Application.swift +++ b/Hotline/Application.swift @@ -55,6 +55,16 @@ struct Application: App { .environment(preferences) } + // MARK: News Editor Window +// WindowGroup(id: "news-editor", for: NewsArticle.self) { $article in +// NewsEditorView(article: $article) +// } +// .windowResizability(.contentSize) +// .windowStyle(.titleBar) +// .windowToolbarStyle(.unifiedCompact(showsTitle: true)) +// .defaultSize(width: 450, height: 550) +// .defaultPosition(.center) + // MARK: Image Preview Window WindowGroup(id: "preview-image", for: PreviewFileInfo.self) { $info in FilePreviewImageView(info: $info) diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift index c33cb75..c1b13ff 100644 --- a/Hotline/Hotline/HotlineClient.swift +++ b/Hotline/Hotline/HotlineClient.swift @@ -131,82 +131,46 @@ class HotlineClient { } switch newState { - case .preparing: - print("HotlineClient: connection preparing...") - case .setup: - print("HotlineClient: connection setup") - case .waiting(let err): - print("HotlineClient: connection waiting \(err)...") - switch err { - case .posix(let errCode): - print("HotlineClient: posix error code \(errCode)") - self.disconnect() -// switch errCode { -// case .ETIMEDOUT: -// self.disconnect() -// case .ECONNREFUSED: -// self.disconnect() -// default: -// self.disconnect() -// break -// } - case .tls(let errStatus): - print("HotlineClient: tls error code \(errStatus)") - self.disconnect() - case .dns(let errType): - print("HotlineClient: DNS Error code \(errType)") - self.disconnect() - default: - print("HotlineClient: error code \(err)") - } case .ready: print("HotlineClient: connection ready!") self.updateConnectionStatus(.connected) self.connectCallback?(true) self.connectCallback = nil -// if self.connectionContinuation != nil { -// let continuation = self.connectionContinuation! -// self.connectionContinuation = nil -// callback?(true) -// continuation.resume(returning: true) +// case .waiting(let err): +// print("HotlineClient: connection waiting \(err)...") +// switch err { +// case .posix(let errCode): +// print("HotlineClient: posix error code \(errCode)") +// self.disconnect() +// case .tls(let errStatus): +// print("HotlineClient: tls error code \(errStatus)") +// self.disconnect() +// case .dns(let errType): +// print("HotlineClient: DNS Error code \(errType)") +// self.disconnect() +// default: +// print("HotlineClient: error code \(err)") // } -// callback?(true) - case .failed(let err): - print("HotlineClient: connection error \(err)") + case .cancelled: + print("HotlineClient: connection cancelled") self.updateConnectionStatus(.disconnected) self.reset() self.connectCallback?(false) self.connectCallback = nil -// callback?(false) -// if self.connectionContinuation != nil { -// let continuation = self.connectionContinuation! -// self.connectionContinuation = nil -// continuation.resume(returning: false) -// } - case .cancelled: - print("HotlineClient: connection cancelled") + case .failed(let err): + print("HotlineClient: connection error \(err)") self.updateConnectionStatus(.disconnected) self.reset() self.connectCallback?(false) self.connectCallback = nil -// callback?(false) -// if self.connectionContinuation != nil { -// let continuation = self.connectionContinuation! -// self.connectionContinuation = nil -// continuation.resume(returning: false) -// } default: + print("HotlineClient: hmm", newState) break } } self.updateConnectionStatus(.connecting) self.connection?.start(queue: .global()) - -// return await withCheckedContinuation { [weak self] continuation in -// self?.connectionContinuation = continuation -// self?.connection?.start(queue: .global()) -// } } private func reset() { @@ -214,6 +178,7 @@ class HotlineClient { } func disconnect() { + print("DISCONNECT?") for (_, replyInfo) in self.transactionLog { let replyCallback = replyInfo.1 replyCallback?(HotlineTransaction(type: replyInfo.0), .networkFailure) @@ -590,6 +555,47 @@ class HotlineClient { }) } + func postNewsArticle(title: String, text: String, path: [String] = [], parentID: UInt32? = nil, sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsArticle]) -> Void)? = nil) { + var t = HotlineTransaction(type: .postNewsArticle) + if !path.isEmpty { + t.setFieldPath(type: .newsPath, val: path) + } + if let parentID = parentID { + t.setFieldUInt32(type: .newsArticleID, val: parentID) + } + t.setFieldString(type: .newsArticleTitle, val: title) + t.setFieldString(type: .newsArticleDataFlavor, val: "text/plain") + t.setFieldUInt32(type: .newsArticleFlags, val: 0) + t.setFieldString(type: .newsArticleData, val: text) + + self.sendTransaction(t, sent: sent, reply: { r, err in + if err != nil { + reply?([]) + return + } + + var articles: [HotlineNewsArticle] = [] +// let articleData = r.getField(type: .newsArticleListData) + +// print("ARTICLE DATA?", articleData) + + if let articleData = r.getField(type: .newsArticleListData) { + 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 sendGetNewsArticles(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsArticle]) -> Void)? = nil) { var t = HotlineTransaction(type: .getNewsArticleNameList) if !path.isEmpty { diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index e88c267..c018e55 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -246,7 +246,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate { self.username = username self.iconID = iconID - print("CLIENT LOGIN: '\(server.login)' '\(server.password == nil)'") + print("CLIENT LOGIN: '\(server.login)' '\(server.password)'") self.client.login(server.address, port: UInt16(server.port), login: server.login, password: server.password, username: username, iconID: UInt16(iconID)) { [weak self] err, serverName, serverVersion in self?.serverVersion = serverVersion diff --git a/Hotline/Models/NewsArticle.swift b/Hotline/Models/NewsArticle.swift new file mode 100644 index 0000000..067d6b6 --- /dev/null +++ b/Hotline/Models/NewsArticle.swift @@ -0,0 +1,21 @@ +import Foundation + +struct NewsArticle: Identifiable, Codable { + var id: UUID = UUID() + var parentID: UInt32? + var path: [String] + var title: String + var body: String +} + +extension NewsArticle: Equatable { + static func == (lhs: NewsArticle, rhs: NewsArticle) -> Bool { + return lhs.id == rhs.id && lhs.parentID == rhs.parentID + } +} + +extension NewsArticle: Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} diff --git a/Hotline/Shared/URLAdditions.swift b/Hotline/Shared/URLAdditions.swift index 7eebb46..1f25541 100644 --- a/Hotline/Shared/URLAdditions.swift +++ b/Hotline/Shared/URLAdditions.swift @@ -23,20 +23,4 @@ extension URL { return filePath } - -// private func prepareDownloadFile(name: String) -> Bool { -// let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] -// let filePath = findUniqueFilePath(base: name, at: folderURL) -// -// if FileManager.default.createFile(atPath: filePath, contents: nil) { -// if let h = FileHandle(forWritingAtPath: filePath) { -// self.filePath = filePath -// self.fileHandle = h -// self.fileProgress?.fileURL = URL(filePath: filePath).resolvingSymlinksInPath() -// return true -// } -// } -// -// return false -// } } diff --git a/Hotline/macOS/FilePreviewTextView.swift b/Hotline/macOS/FilePreviewTextView.swift index 4c81e6f..8b5b4f3 100644 --- a/Hotline/macOS/FilePreviewTextView.swift +++ b/Hotline/macOS/FilePreviewTextView.swift @@ -43,8 +43,8 @@ struct FilePreviewTextView: View { .contentMargins(.top, -16.0, for: .scrollIndicators) .contentMargins(.bottom, -16.0, for: .scrollIndicators) .contentMargins(.trailing, -16.0, for: .scrollIndicators) - .frame(maxWidth: .infinity, maxHeight: .infinity) .scrollClipDisabled() + .frame(maxWidth: .infinity, maxHeight: .infinity) } else { VStack(alignment: .center, spacing: 0) { diff --git a/Hotline/macOS/NewsEditorView.swift b/Hotline/macOS/NewsEditorView.swift new file mode 100644 index 0000000..18c4a0d --- /dev/null +++ b/Hotline/macOS/NewsEditorView.swift @@ -0,0 +1,100 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct NewsEditorView: View { + @Environment(\.controlActiveState) private var controlActiveState + @Environment(\.colorScheme) private var colorScheme + @Environment(\.dismiss) private var dismiss + @Environment(Hotline.self) private var model: Hotline + +// @Binding var article: NewsArticle? + @State var title: String = "" + @State var text: String = "" + + var body: some View { + NavigationStack { + VStack(alignment: .leading, spacing: 0) { + HStack { + Button { + dismiss() + } label: { + Image(systemName: "xmark.circle.fill") + .resizable() + .scaledToFit() + } + .buttonStyle(.plain) + .frame(width: 16, height: 16) + .padding() + + Spacer() + + Button { + } label: { + Image(systemName: "paperplane") + .resizable() + .scaledToFit() + } + .buttonStyle(.plain) + .frame(width: 16, height: 16) + .padding() + } + .frame(maxWidth: .infinity) + TextField("Title", text: $title, axis: .vertical) + .textFieldStyle(.plain) + .padding() + .focusEffectDisabled() + .font(.title) + .frame(maxWidth: .infinity) + .border(Color.pink, width: 0) + Divider() + TextEditor(text: $text) + .textEditorStyle(.plain) + .font(.system(size: 14)) + .lineSpacing(3) + .padding(16) + .contentMargins(.top, -16.0, for: .scrollIndicators) + .contentMargins(.bottom, -16.0, for: .scrollIndicators) + .contentMargins(.trailing, -16.0, for: .scrollIndicators) + .scrollClipDisabled() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .frame(minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 300, idealHeight: 500, maxHeight: .infinity) + .background(Color(nsColor: .textBackgroundColor)) + .presentationCompactAdaptation(.sheet) + .toolbarTitleDisplayMode(.inlineLarge) +// .toolbar { +// ToolbarItem(placement: .navigation) { +// Button("Post", action: {}) +// } +// ToolbarItem(placement: .automatic) { +// Button("Delete", action: { +// dismiss() +// }) +// } +// } + .task { +// if let info = info { +// preview = FilePreview(info: info) +// preview?.download() +// } + } + .onAppear { +// if info == nil { +// Task { +// dismiss() +// } +// return +// } + } + .onDisappear { +// preview?.cancel() + dismiss() + } +// .onChange(of: preview?.state) { +// if preview?.state == .failed { +// dismiss() +// } +// } + } +} diff --git a/Hotline/macOS/NewsView.swift b/Hotline/macOS/NewsView.swift index daeb45b..d7e40f2 100644 --- a/Hotline/macOS/NewsView.swift +++ b/Hotline/macOS/NewsView.swift @@ -1,5 +1,190 @@ import SwiftUI import MarkdownUI +import SplitView + +struct NewsView: View { + @Environment(Hotline.self) private var model: Hotline + @Environment(\.openWindow) private var openWindow + + @State private var selection: NewsInfo? + @State private var articleText: String? + @State private var splitHidden = SideHolder(.bottom) + @State private var splitFraction = FractionHolder.usingUserDefaults(0.25, key: "News Split Fraction") + @State private var editorOpen: Bool = false + + var body: some View { + NavigationStack { + VSplit( + top: { + newsBrowser + }, + bottom: { + articleViewer + } + ) + .fraction(splitFraction) + .constraints(minPFraction: 0.1, minSFraction: 0.3) + .hide(splitHidden) + .styling(inset: 0, visibleThickness: 0.5, invisibleThickness: 5, hideSplitter: true) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(nsColor: .textBackgroundColor)) + } +// .sheet(isPresented: $editorOpen) { +// print("Sheet dismissed!") +// } content: { +// NewsEditorView() +// } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + + } label: { + Image(systemName: "trash") + } + } + + ToolbarItem(placement: .primaryAction) { + Button { +// if let selection = selection { +// editorOpen = true +//// openWindow(id: "news-editor", value: NewsArticle(parentID: nil, path: selection.path, title: "", body: "")) +// } + } label: { + Image(systemName: "square.and.pencil") + } + } + + ToolbarItem(placement: .primaryAction) { + Button { + + } label: { + Image(systemName: "arrowshape.turn.up.left") + } + } + } + } + + var newsBrowser: some View { + List(model.news, id: \.self, selection: $selection) { newsItem in + NewsItemView(news: newsItem, depth: 0).tag(newsItem.id) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .environment(\.defaultMinListRowHeight, 34) + .listStyle(.inset) + .alternatingRowBackgrounds(.enabled) + .task { + if !model.newsLoaded { + let _ = await model.getNewsList() + } + } + .contextMenu(forSelectionType: NewsInfo.self) { items in + // ... + } primaryAction: { items in + guard let clickedNews = items.first else { + return + } + + self.selection = clickedNews + if clickedNews.type == .bundle || clickedNews.type == .category { + clickedNews.expanded.toggle() + } + } + .onChange(of: selection) { + self.articleText = nil + if let article = selection, article.type == .article { + if 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 + } + } + if self.splitHidden.side != nil { + withAnimation(.easeOut(duration: 0.15)) { + self.splitHidden.side = nil + } + } + + } + } + else { + if self.splitHidden.side != .bottom { + withAnimation(.easeOut(duration: 0.25)) { + self.splitHidden.side = .bottom + } + } + } + } + .onKeyPress(.rightArrow) { + if let s = selection, s.type == .bundle || s.type == .category { + s.expanded = true + return .handled + } + return .ignored + } + .onKeyPress(.leftArrow) { + if let s = selection, s.type == .bundle || s.type == .category { + s.expanded = false + return .handled + } + return .ignored + } + .overlay { + if !model.newsLoaded { + VStack { + ProgressView() + .controlSize(.regular) + } + .frame(maxWidth: .infinity) + } + } + } + + var articleViewer: some View { + ScrollView { + if let selection = selection { + VStack(alignment: .leading, spacing: 0) { + if let poster = selection.articleUsername, let postDate = selection.articleDate { + HStack(alignment: .firstTextBaseline) { + Text(poster) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.tail) + .textSelection(.enabled) + .padding(.bottom, 16) + Spacer() + Text("\(NewsItemView.dateFormatter.string(from: postDate))") + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.tail) + .textSelection(.enabled) + .padding(.bottom, 16) + } + } + + Divider() + + Text(selection.name).font(.title) + .textSelection(.enabled) + .padding(.bottom, 8) + .padding(.top, 16) + + if let newsText = self.articleText { + Markdown(newsText) + .markdownTheme(.basic) + .textSelection(.enabled) + .lineSpacing(6) + .padding(.top, 16) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .padding() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .transition(.move(edge: .bottom)) + } +} struct NewsItemView: View { @Environment(Hotline.self) private var model: Hotline @@ -47,6 +232,7 @@ struct NewsItemView: View { .scaledToFit() .frame(width: 10) .foregroundStyle(.secondary) + .opacity(0.5) } } Text(news.name) @@ -93,178 +279,6 @@ struct NewsItemView: View { } } -struct NewsView: View { - @Environment(Hotline.self) private var model: Hotline - - @State private var selection: NewsInfo? - @State private var articleText: String? - - var body: some View { - NavigationStack { - VSplitView { - - // MARK: News Browser - List(model.news, id: \.self, selection: $selection) { newsItem in - NewsItemView(news: newsItem, depth: 0).tag(newsItem.id) - } - - .frame(maxWidth: .infinity, minHeight: 100, idealHeight: 100) - .environment(\.defaultMinListRowHeight, 34) - .listStyle(.inset) - .alternatingRowBackgrounds(.enabled) - .task { - if !model.newsLoaded { - let _ = await model.getNewsList() - } - } - .contextMenu(forSelectionType: NewsInfo.self) { items in - // ... - } primaryAction: { items in - print("ITEMS?", items) - guard let clickedNews = items.first else { - return - } - - self.selection = clickedNews - if clickedNews.type == .bundle || clickedNews.type == .category { - clickedNews.expanded.toggle() - } - } - .onChange(of: selection) { - if - let article = selection, - article.type == .article { - self.articleText = nil - 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 - } - } - } - } - } - .onKeyPress(.rightArrow) { - if let s = selection, s.type == .bundle || s.type == .category { - s.expanded = true - return .handled - } - return .ignored - } - .onKeyPress(.leftArrow) { - if let s = selection, s.type == .bundle || s.type == .category { - s.expanded = false - return .handled - } - return .ignored - } - .overlay { - if !model.newsLoaded { - VStack { - ProgressView() - .controlSize(.large) - } - .frame(maxWidth: .infinity) - } - } - - // MARK: Article Viewer - ScrollView { - VStack(alignment: .leading, spacing: 0) { - if let news = selection { - if news.type == .article { - -// Text(news.name).font(.title) -// .textSelection(.enabled) -// .padding(.bottom, 8) - - if let poster = news.articleUsername, let postDate = news.articleDate { - HStack(alignment: .firstTextBaseline) { - Text(poster) - .foregroundStyle(.secondary) - .lineLimit(1) - .truncationMode(.tail) - .textSelection(.enabled) - .padding(.bottom, 16) - Spacer() - Text("\(NewsItemView.dateFormatter.string(from: postDate))") - .foregroundStyle(.secondary) - .lineLimit(1) - .truncationMode(.tail) - .textSelection(.enabled) - .padding(.bottom, 16) - } - } - - Divider() - - if let newsText = self.articleText { - Markdown(newsText) -// Text(newsText) - .textSelection(.enabled) - .lineSpacing(4) - .padding(.top, 16) - } - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - } - .frame(maxWidth: .infinity, minHeight: 200) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(nsColor: .textBackgroundColor)) - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button { - - } label: { - Image(systemName: "square.and.pencil") - } - } - - ToolbarItem(placement: .primaryAction) { - Button { - - } label: { - Image(systemName: "arrowshape.turn.up.left") - } - } - -// if let bannerImage = model.bannerImage { -// ToolbarItem(placement: .primaryAction) { -//// HStack { -// bannerImage -// .resizable() -// .aspectRatio(contentMode: .fit) -// .clipped() -// .frame(minHeight: 60, alignment: .topLeading -// ) -// .clipped() -// } -// .frame(maxWidth: .infinity) -// .tag(MenuItem(name: "title", image: "", type: .banner)) -// .padding(.bottom, 16) - - // VStack(spacing: 0) { - // bannerImage - // .resizable() - // .aspectRatio(contentMode: .fit) - // .frame(maxWidth: .infinity, minHeight: 60, alignment: .topLeading) - // .clipped() - // } - // .frame(maxWidth: .infinity) -// } -// } - } - } -} - #Preview { NewsView() .environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())) diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift index 268cd3e..23e6da2 100644 --- a/Hotline/macOS/TrackerView.swift +++ b/Hotline/macOS/TrackerView.swift @@ -237,7 +237,7 @@ struct TrackerView: View { var bookmarks: [TrackerBookmark] = [ TrackerBookmark(type: .server, name: "RetroMac", address: "82.32.130.173"), - TrackerBookmark(type: .server, name: "System 7 Today", address: "158.174.146.146"), + TrackerBookmark(type: .server, name: "System 7 Today", address: "62.116.228.147"), TrackerBookmark(type: .tracker, name: "Featured Servers", address: "hltracker.com"), TrackerBookmark(type: .tracker, name: "Agent79", address: "tracked.agent79.org"), TrackerBookmark(type: .tracker, name: "Preterhuman", address: "tracker.preterhuman.net"), |