aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline.xcodeproj/project.pbxproj29
-rw-r--r--Hotline.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved9
-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
15 files changed, 499 insertions, 327 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index 8358f3b..d1448ee 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 */,
@@ -496,7 +508,7 @@
CODE_SIGN_ENTITLEMENTS = Hotline/Hotline.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "\"Hotline/Preview Content\"";
DEVELOPMENT_TEAM = 5AEEV7QB2U;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
@@ -535,7 +547,7 @@
CODE_SIGN_ENTITLEMENTS = Hotline/Hotline.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 7;
+ CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "\"Hotline/Preview Content\"";
DEVELOPMENT_TEAM = 5AEEV7QB2U;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
@@ -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..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()