diff options
| -rw-r--r-- | Hotline.xcodeproj/project.pbxproj | 25 | ||||
| -rw-r--r-- | Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 9 | ||||
| -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 |
11 files changed, 415 insertions, 246 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj index 8358f3b..b6ef33b 100644 --- a/Hotline.xcodeproj/project.pbxproj +++ b/Hotline.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ DA5753682B33E88A00FAC277 /* HotlineFileClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */; }; DA57536C2B36BA1D00FAC277 /* TextDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA57536B2B36BA1D00FAC277 /* TextDocument.swift */; }; DA6300972B24036B0034CBFD /* HotlineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6300962B24036B0034CBFD /* HotlineClient.swift */; }; + DA72A0DD2B4CD0BF00A0F48A /* NewsEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA72A0DC2B4CD0BF00A0F48A /* NewsEditorView.swift */; }; + DA72A0E02B4DA8CA00A0F48A /* SplitView in Frameworks */ = {isa = PBXBuildFile; productRef = DA72A0DF2B4DA8CA00A0F48A /* SplitView */; }; + DA72A0E22B4DAA4000A0F48A /* NewsArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA72A0E12B4DAA4000A0F48A /* NewsArticle.swift */; }; DA77253F2B21176D006C5ABB /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA77253E2B21176D006C5ABB /* NewsView.swift */; platformFilter = ios; }; DA7725412B21435B006C5ABB /* ObservableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7725402B21435B006C5ABB /* ObservableScrollView.swift */; }; DA9CAFBB2B126D5700CDA197 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9CAFBA2B126D5700CDA197 /* Application.swift */; }; @@ -74,6 +77,8 @@ DA5753672B33E88A00FAC277 /* HotlineFileClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineFileClient.swift; sourceTree = "<group>"; }; DA57536B2B36BA1D00FAC277 /* TextDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextDocument.swift; sourceTree = "<group>"; }; DA6300962B24036B0034CBFD /* HotlineClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotlineClient.swift; sourceTree = "<group>"; }; + DA72A0DC2B4CD0BF00A0F48A /* NewsEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsEditorView.swift; sourceTree = "<group>"; }; + DA72A0E12B4DAA4000A0F48A /* NewsArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsArticle.swift; sourceTree = "<group>"; }; DA77253E2B21176D006C5ABB /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = "<group>"; }; DA7725402B21435B006C5ABB /* ObservableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableScrollView.swift; sourceTree = "<group>"; }; DA9CAFB72B126D5700CDA197 /* Hotline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hotline.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -111,6 +116,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DA72A0E02B4DA8CA00A0F48A /* SplitView in Frameworks */, DAB4D8872B4CB3610048A05C /* MarkdownUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -222,6 +228,7 @@ DA2863DC2B3E8B7000A7D050 /* FilePreview.swift */, DA32CD4C2B2931B50053B98B /* ChatMessage.swift */, DA32CD4E2B2931CC0053B98B /* NewsInfo.swift */, + DA72A0E12B4DAA4000A0F48A /* NewsArticle.swift */, DAAEE66C2B475F1400A5BA07 /* PreviewFileInfo.swift */, ); path = Models; @@ -239,6 +246,7 @@ DAE735002B2E71F2000C56F6 /* FilesView.swift */, DAAEE66E2B47625600A5BA07 /* FilePreviewImageView.swift */, DAB4D87D2B4C8BCA0048A05C /* FilePreviewTextView.swift */, + DA72A0DC2B4CD0BF00A0F48A /* NewsEditorView.swift */, DA2863D72B37AD1C00A7D050 /* SettingsView.swift */, ); path = macOS; @@ -262,6 +270,7 @@ name = Hotline; packageProductDependencies = ( DAB4D8862B4CB3610048A05C /* MarkdownUI */, + DA72A0DF2B4DA8CA00A0F48A /* SplitView */, ); productName = Hotline; productReference = DA9CAFB72B126D5700CDA197 /* Hotline.app */; @@ -293,6 +302,7 @@ mainGroup = DA9CAFAE2B126D5700CDA197; packageReferences = ( DAB4D8852B4CB3610048A05C /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, + DA72A0DE2B4DA8CA00A0F48A /* XCRemoteSwiftPackageReference "SplitView" */, ); productRefGroup = DA9CAFB82B126D5700CDA197 /* Products */; projectDirPath = ""; @@ -333,6 +343,7 @@ DAC002192B21630900A6C290 /* SwiftUIExtensions.swift in Sources */, DADDB28F2B238D850024040D /* Hotline.swift in Sources */, DADDB28D2B22B5920024040D /* Server.swift in Sources */, + DA72A0E22B4DAA4000A0F48A /* NewsArticle.swift in Sources */, DAE734FB2B2E41F9000C56F6 /* TrackerView.swift in Sources */, DA9CAFBD2B126D5700CDA197 /* TrackerView.swift in Sources */, DAB4D87B2B4B78310048A05C /* AnimatedImageView.swift in Sources */, @@ -346,6 +357,7 @@ DA0D698F2B1E841600C71DF5 /* MessageBoardView.swift in Sources */, DAE735072B3251B3000C56F6 /* Utilities.swift in Sources */, DAE735012B2E71F2000C56F6 /* FilesView.swift in Sources */, + DA72A0DD2B4CD0BF00A0F48A /* NewsEditorView.swift in Sources */, DABFCC292B1530DC009F40D2 /* FoundationExtensions.swift in Sources */, DA57536C2B36BA1D00FAC277 /* TextDocument.swift in Sources */, DA77253F2B21176D006C5ABB /* NewsView.swift in Sources */, @@ -590,6 +602,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + DA72A0DE2B4DA8CA00A0F48A /* XCRemoteSwiftPackageReference "SplitView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/stevengharris/SplitView"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.5.0; + }; + }; DAB4D8852B4CB3610048A05C /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; @@ -601,6 +621,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + DA72A0DF2B4DA8CA00A0F48A /* SplitView */ = { + isa = XCSwiftPackageProductDependency; + package = DA72A0DE2B4DA8CA00A0F48A /* XCRemoteSwiftPackageReference "SplitView" */; + productName = SplitView; + }; DAB4D8862B4CB3610048A05C /* MarkdownUI */ = { isa = XCSwiftPackageProductDependency; package = DAB4D8852B4CB3610048A05C /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; diff --git a/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a1afabf..125ba89 100644 --- a/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,15 @@ } }, { + "identity" : "splitview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stevengharris/SplitView", + "state" : { + "revision" : "13ff85ae02a01ae257c0a5cf8b6eb1c2a9fff597", + "version" : "3.5.0" + } + }, + { "identity" : "swift-markdown-ui", "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/swift-markdown-ui", 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"), |