aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <mierau@users.noreply.github.com>2024-01-09 14:44:14 -0800
committerGitHub <noreply@github.com>2024-01-09 14:44:14 -0800
commita2b7946804f5ab17ef1aebc4b5bb72e7cf14e83b (patch)
tree218012a1565033fa89a862a34a0683a9fad3d39b /Hotline
parentf198d4d59b922e9cb872fe6831d3475c570fa56f (diff)
parent60f90281bf971600733b2d72b747bcb6be9b1c31 (diff)
Merge branch 'main' into patch-1
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Application.swift10
-rw-r--r--Hotline/Hotline/HotlineClient.swift116
-rw-r--r--Hotline/Models/Hotline.swift8
-rw-r--r--Hotline/Models/NewsArticle.swift21
-rw-r--r--Hotline/Shared/URLAdditions.swift16
-rw-r--r--Hotline/macOS/ChatView.swift1
-rw-r--r--Hotline/macOS/FilePreviewImageView.swift1
-rw-r--r--Hotline/macOS/FilePreviewTextView.swift3
-rw-r--r--Hotline/macOS/FilesView.swift1
-rw-r--r--Hotline/macOS/MessageBoardView.swift2
-rw-r--r--Hotline/macOS/NewsEditorView.swift100
-rw-r--r--Hotline/macOS/NewsView.swift358
-rw-r--r--Hotline/macOS/ServerView.swift151
13 files changed, 463 insertions, 325 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..ccb0c4c 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() {
@@ -228,7 +192,6 @@ class HotlineClient {
private func sendTransaction(_ t: HotlineTransaction, sent sentCallback: ((Bool) -> Void)? = nil, reply replyCallback: ((HotlineTransaction, HotlineTransactionError?) -> Void)? = nil) {
guard let c = connection else {
- print("NO CONNECTION?????")
sentCallback?(false)
return
}
@@ -590,6 +553,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..e62ac15 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -246,8 +246,6 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate {
self.username = username
self.iconID = iconID
- print("CLIENT LOGIN: '\(server.login)' '\(server.password == nil)'")
-
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
if serverName != nil {
@@ -335,8 +333,6 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate {
continuation.resume(returning: nil)
return
}
-
- print("GET NEWS CATS FROM \(path)")
}, reply: { articleText in
// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
@@ -376,8 +372,6 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate {
continuation.resume(returning: [])
return
}
-
- print("GET NEWS CATS FROM \(path)")
}, reply: { [weak self] categories in
// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
@@ -445,8 +439,6 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate {
}
return
}
-
- print("GET NEWS CATS FROM \(path)")
}, reply: { [weak self] categories in
let parentNews = self?.findNews(in: self?.news ?? [], at: path)
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/ChatView.swift b/Hotline/macOS/ChatView.swift
index 2c154d3..7f7d677 100644
--- a/Hotline/macOS/ChatView.swift
+++ b/Hotline/macOS/ChatView.swift
@@ -118,7 +118,6 @@ struct ChatView: View {
// withAnimation {
reader.scrollTo(bottomID, anchor: .bottom)
// }
- print("SCROLLED TO BOTTOM")
}
.onAppear {
reader.scrollTo(bottomID, anchor: .bottom)
diff --git a/Hotline/macOS/FilePreviewImageView.swift b/Hotline/macOS/FilePreviewImageView.swift
index 1ee4e9b..461f5a4 100644
--- a/Hotline/macOS/FilePreviewImageView.swift
+++ b/Hotline/macOS/FilePreviewImageView.swift
@@ -13,7 +13,6 @@ struct FilePreviewImageView: View {
@Binding var info: PreviewFileInfo?
@State var preview: FilePreview? = nil
- @Namespace var mainNamespace
@FocusState private var focusField: FilePreviewFocus?
var body: some View {
diff --git a/Hotline/macOS/FilePreviewTextView.swift b/Hotline/macOS/FilePreviewTextView.swift
index 4c81e6f..2cb192c 100644
--- a/Hotline/macOS/FilePreviewTextView.swift
+++ b/Hotline/macOS/FilePreviewTextView.swift
@@ -12,7 +12,6 @@ struct FilePreviewTextView: View {
@Binding var info: PreviewFileInfo?
@State var preview: FilePreview? = nil
- @Namespace var mainNamespace
@FocusState private var focusField: FilePreviewFocus?
var body: some View {
@@ -43,8 +42,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/FilesView.swift b/Hotline/macOS/FilesView.swift
index 1eab0cf..b9f15db 100644
--- a/Hotline/macOS/FilesView.swift
+++ b/Hotline/macOS/FilesView.swift
@@ -218,7 +218,6 @@ struct FilesView: View {
ToolbarItem(placement: .primaryAction) {
Button {
if let s = selection, !s.isFolder {
- print("DOWNLOAD FILE", s.name, s.path)
model.downloadFile(s.name, path: s.path)
}
} label: {
diff --git a/Hotline/macOS/MessageBoardView.swift b/Hotline/macOS/MessageBoardView.swift
index d72b122..65a947f 100644
--- a/Hotline/macOS/MessageBoardView.swift
+++ b/Hotline/macOS/MessageBoardView.swift
@@ -22,8 +22,6 @@ struct MessageBoardView: View {
.task {
if !model.messageBoardLoaded {
let _ = await model.getMessageBoard()
-// self.initialLoadComplete = true
- print("INITIAL LOAD?")
}
}
.overlay {
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/ServerView.swift b/Hotline/macOS/ServerView.swift
index 366fff2..b21e4fb 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -171,88 +171,107 @@ struct ServerView: View {
MenuItem(name: "Files", image: "folder", type: .files),
]
+ enum FocusFields {
+ case address
+ case login
+ case password
+ }
+
+ @FocusState private var focusedField: FocusFields?
+
var connectForm: some View {
- GroupBox {
- Form {
- Group {
- TextField(text: $connectAddress) {
- Text("Address:")
- }
- TextField(text: $connectLogin, prompt: Text("optional")) {
- Text("Login:")
+ VStack(alignment: .center) {
+ GroupBox {
+ Form {
+ Group {
+ TextField(text: $connectAddress) {
+ Text("Address:")
+ }
+ .focused($focusedField, equals: .address)
+
+ Text("Type the address of the Hotline server you would like to connect to. If you have an account on that server, type your login and password too.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+
+ TextField(text: $connectLogin, prompt: Text("optional")) {
+ Text("Login:")
+ }
+ .focused($focusedField, equals: .login)
+ SecureField(text: $connectPassword, prompt: Text("optional")) {
+ Text("Password:")
+ }
+ .focused($focusedField, equals: .password)
}
- SecureField(text: $connectPassword, prompt: Text("optional")) {
- Text("Password:")
+ .textFieldStyle(.roundedBorder)
+ .controlSize(.regular)
+
+ HStack {
+ Button {
+ print("SAVE BOOKMARK... SOMEHOW")
+ } label: {
+ Text("Save...")
+ }
+ .controlSize(.regular)
+ .buttonStyle(.automatic)
+ .help("Save server as bookmark")
+
+ Spacer()
+
+ Button {
+ dismiss()
+ } label: {
+ Text("Cancel")
+ }
+ .controlSize(.regular)
+ .buttonStyle(.automatic)
+ .keyboardShortcut(.cancelAction)
+
+ Button {
+
+ // if var s = server {
+ // print("CHANGING EXISTING SERVER")
+ // s.name = newServer.name
+ // s.description = newServer.description
+ // s.users = newServer.users
+ // s.address = newServer.address
+ // s.port = newServer.port
+ // s.login = newServer.login
+ // s.password = newServer.password
+ // }
+ // else {
+ // server = newServer
+ // }
+
+ Task {
+ await connectToServer()
+ }
+ } label: {
+ Text("Connect")
+ }
+ .controlSize(.regular)
+ .buttonStyle(.automatic)
+ .keyboardShortcut(.defaultAction)
}
+ .padding(.top, 8)
+
}
- .textFieldStyle(.roundedBorder)
- .controlSize(.regular)
+ .padding()
.onChange(of: connectAddress) {
let (a, p) = Server.parseServerAddressAndPort(connectAddress)
server.address = a
server.port = p
- print("ADDRESS CHANGED: '\(connectAddress)' \(a) \(p)")
}
.onChange(of: connectLogin) {
server.login = connectLogin.trimmingCharacters(in: .whitespacesAndNewlines)
- print("LOGIN CHANGED: '\(connectLogin)'" + $server.wrappedValue.login)
}
.onChange(of: connectPassword) {
server.password = connectPassword
- print("PASS CHANGED: '\(connectPassword)'" + server.password)
- }
-
- HStack {
- Button {
- print("SAVE BOOKMARK... SOMEHOW")
- } label: {
- Text("Save...")
- }
- .controlSize(.regular)
- .buttonStyle(.automatic)
- .help("Save server as bookmark")
-
- Spacer()
-
- Button {
- dismiss()
- } label: {
- Text("Cancel")
- }
- .controlSize(.regular)
- .buttonStyle(.automatic)
- .keyboardShortcut(.cancelAction)
-
- Button {
-
-// if var s = server {
-// print("CHANGING EXISTING SERVER")
-// s.name = newServer.name
-// s.description = newServer.description
-// s.users = newServer.users
-// s.address = newServer.address
-// s.port = newServer.port
-// s.login = newServer.login
-// s.password = newServer.password
-// }
-// else {
-// server = newServer
-// }
-
- Task {
- await connectToServer()
- }
- } label: {
- Text("Connect")
- }
- .controlSize(.regular)
- .buttonStyle(.automatic)
- .keyboardShortcut(.defaultAction)
}
- .padding(.top, 8)
-
}
- .padding()
+// .padding(.top, 8)
+ .onAppear {
+ focusedField = .address
+ }
}
.frame(maxWidth: 350)
.padding()