aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline/Assets.xcassets/Server Large.imageset/Server Large.pngbin1617 -> 1292 bytes
-rw-r--r--Hotline/Assets.xcassets/Server Large.imageset/Server Large@2x.pngbin4044 -> 2689 bytes
-rw-r--r--Hotline/Assets.xcassets/Server Large.imageset/Server Large@3x.pngbin6988 -> 4259 bytes
-rw-r--r--Hotline/Assets.xcassets/Server Message.imageset/4016.pngbin827 -> 812 bytes
-rw-r--r--Hotline/Assets.xcassets/Server Message.imageset/4016@2x.pngbin1611 -> 1550 bytes
-rw-r--r--Hotline/Assets.xcassets/Server Message.imageset/4016@3x.pngbin2430 -> 2350 bytes
-rw-r--r--Hotline/Assets.xcassets/Server.imageset/Server.pngbin900 -> 842 bytes
-rw-r--r--Hotline/Assets.xcassets/Server.imageset/Server@2x.pngbin2051 -> 1778 bytes
-rw-r--r--Hotline/Assets.xcassets/Server.imageset/Server@3x.pngbin3433 -> 2813 bytes
-rw-r--r--Hotline/Assets.xcassets/User.imageset/User.pngbin479 -> 433 bytes
-rw-r--r--Hotline/Assets.xcassets/User.imageset/User@2x.pngbin824 -> 732 bytes
-rw-r--r--Hotline/Assets.xcassets/User.imageset/User@3x.pngbin1202 -> 1050 bytes
-rw-r--r--Hotline/Hotline/HotlineClient.swift33
-rw-r--r--Hotline/Models/Hotline.swift220
-rw-r--r--Hotline/Models/NewsInfo.swift44
-rw-r--r--Hotline/Utility/FoundationExtensions.swift7
-rw-r--r--Hotline/iOS/NewsView.swift2
-rw-r--r--Hotline/macOS/AboutView.swift13
-rw-r--r--Hotline/macOS/ChatView.swift8
-rw-r--r--Hotline/macOS/FileDetailsView.swift2
-rw-r--r--Hotline/macOS/FilePreviewImageView.swift4
-rw-r--r--Hotline/macOS/FilesView.swift18
-rw-r--r--Hotline/macOS/HotlinePanelView.swift3
-rw-r--r--Hotline/macOS/NewsEditorView.swift189
-rw-r--r--Hotline/macOS/NewsView.swift65
-rw-r--r--Hotline/macOS/ServerMessageView.swift6
26 files changed, 398 insertions, 216 deletions
diff --git a/Hotline/Assets.xcassets/Server Large.imageset/Server Large.png b/Hotline/Assets.xcassets/Server Large.imageset/Server Large.png
index 25a97b0..0b5fbb7 100644
--- a/Hotline/Assets.xcassets/Server Large.imageset/Server Large.png
+++ b/Hotline/Assets.xcassets/Server Large.imageset/Server Large.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Server Large.imageset/Server Large@2x.png b/Hotline/Assets.xcassets/Server Large.imageset/Server Large@2x.png
index 63fe295..d99e32c 100644
--- a/Hotline/Assets.xcassets/Server Large.imageset/Server Large@2x.png
+++ b/Hotline/Assets.xcassets/Server Large.imageset/Server Large@2x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Server Large.imageset/Server Large@3x.png b/Hotline/Assets.xcassets/Server Large.imageset/Server Large@3x.png
index 7b8cd6e..ce32a16 100644
--- a/Hotline/Assets.xcassets/Server Large.imageset/Server Large@3x.png
+++ b/Hotline/Assets.xcassets/Server Large.imageset/Server Large@3x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Server Message.imageset/4016.png b/Hotline/Assets.xcassets/Server Message.imageset/4016.png
index 97e96a7..a11fbea 100644
--- a/Hotline/Assets.xcassets/Server Message.imageset/4016.png
+++ b/Hotline/Assets.xcassets/Server Message.imageset/4016.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Server Message.imageset/4016@2x.png b/Hotline/Assets.xcassets/Server Message.imageset/4016@2x.png
index bb97e20..ef90260 100644
--- a/Hotline/Assets.xcassets/Server Message.imageset/4016@2x.png
+++ b/Hotline/Assets.xcassets/Server Message.imageset/4016@2x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Server Message.imageset/4016@3x.png b/Hotline/Assets.xcassets/Server Message.imageset/4016@3x.png
index d17f423..a2646ee 100644
--- a/Hotline/Assets.xcassets/Server Message.imageset/4016@3x.png
+++ b/Hotline/Assets.xcassets/Server Message.imageset/4016@3x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Server.imageset/Server.png b/Hotline/Assets.xcassets/Server.imageset/Server.png
index 3b63a79..f8d8bcf 100644
--- a/Hotline/Assets.xcassets/Server.imageset/Server.png
+++ b/Hotline/Assets.xcassets/Server.imageset/Server.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Server.imageset/Server@2x.png b/Hotline/Assets.xcassets/Server.imageset/Server@2x.png
index ede8ce0..effc0f5 100644
--- a/Hotline/Assets.xcassets/Server.imageset/Server@2x.png
+++ b/Hotline/Assets.xcassets/Server.imageset/Server@2x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/Server.imageset/Server@3x.png b/Hotline/Assets.xcassets/Server.imageset/Server@3x.png
index eb3e466..5b17b06 100644
--- a/Hotline/Assets.xcassets/Server.imageset/Server@3x.png
+++ b/Hotline/Assets.xcassets/Server.imageset/Server@3x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/User.imageset/User.png b/Hotline/Assets.xcassets/User.imageset/User.png
index b675f22..c69a204 100644
--- a/Hotline/Assets.xcassets/User.imageset/User.png
+++ b/Hotline/Assets.xcassets/User.imageset/User.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/User.imageset/User@2x.png b/Hotline/Assets.xcassets/User.imageset/User@2x.png
index a5d59af..03f73e9 100644
--- a/Hotline/Assets.xcassets/User.imageset/User@2x.png
+++ b/Hotline/Assets.xcassets/User.imageset/User@2x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/User.imageset/User@3x.png b/Hotline/Assets.xcassets/User.imageset/User@3x.png
index cd58773..ec35530 100644
--- a/Hotline/Assets.xcassets/User.imageset/User@3x.png
+++ b/Hotline/Assets.xcassets/User.imageset/User@3x.png
Binary files differ
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift
index 07f24a4..1c72ab0 100644
--- a/Hotline/Hotline/HotlineClient.swift
+++ b/Hotline/Hotline/HotlineClient.swift
@@ -556,35 +556,28 @@ class HotlineClient: NetSocketDelegate {
}
}
- @MainActor func postNewsArticle(title: String, text: String, path: [String] = [], parentID: UInt32? = nil, callback: (([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)
+ @MainActor func postNewsArticle(title: String, text: String, path: [String] = [], parentID: UInt32 = 0, callback: ((Bool) -> Void)? = nil) {
+ guard !path.isEmpty else {
+ callback?(false)
+ return
}
+
+ var t = HotlineTransaction(type: .postNewsArticle)
+ t.setFieldPath(type: .newsPath, val: path)
+ 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)
+ print("HotlineClient postings \(title) under \(parentID)")
+
self.sendPacket(t) { reply, err in
- guard err == nil,
- let articleData = reply.getField(type: .newsArticleListData) else {
- callback?([])
+ guard err == nil else {
+ callback?(false)
return
}
-
- var articles: [HotlineNewsArticle] = []
- let newsList = articleData.getNewsList()
- for art in newsList.articles {
- var blah = art
- blah.path = path
- articles.append(blah)
- }
-
- callback?(articles)
+ callback?(true)
}
}
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index 5dc61ab..4c9defa 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -131,6 +131,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
var files: [FileInfo] = []
var filesLoaded: Bool = false
var news: [NewsInfo] = []
+ private var newsLookup: [String:NewsInfo] = [:]
var newsLoaded: Bool = false
var instantMessages: [UInt16:[InstantMessage]] = [:]
var transfers: [TransferInfo] = []
@@ -307,88 +308,110 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
}
- @MainActor func getNewsList(at path: [String] = []) async -> [NewsInfo] {
+ @MainActor func getNewsList(at path: [String] = []) async {
return await withCheckedContinuation { [weak self] continuation in
- var requestCategories = true
+ let parentNewsGroup = self?.findNews(in: self?.news ?? [], at: path)
- let existingNewsItem = self?.findNews(in: self?.news ?? [], at: path)
-
- if existingNewsItem != nil {
- if existingNewsItem!.type != .bundle {
- requestCategories = false
- }
- }
-
- if requestCategories {
- self?.client.sendGetNewsCategories(path: path) { [weak self] categories in
-// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
+ // Send a categories request for bundle paths or root (empty path)
+ if path.isEmpty || parentNewsGroup?.type == .bundle {
+ print("Hotline: Requesting categories at: /\(path.joined(separator: "/"))")
+
+ self?.client.sendGetNewsCategories(path: path) { @MainActor [weak self] categories in
+ // Create info for each category returned.
+ var newCategoryInfos: [NewsInfo] = []
- var newCategories: [NewsInfo] = []
+ // Transform hotline categories into NewsInfo objects.
for category in categories {
- newCategories.append(NewsInfo(hotlineNewsCategory: category))
- }
-
- DispatchQueue.main.async {
- if let parent = existingNewsItem {
- parent.children = newCategories
- }
- else if path.isEmpty {
- self?.newsLoaded = true
- self?.news = newCategories
+ var newsCategoryInfo = NewsInfo(hotlineNewsCategory: category)
+
+ if let lookupPath = newsCategoryInfo.lookupPath {
+ // Merge returned category info with existing category info.
+ if let existingCategoryInfo = self?.newsLookup[lookupPath] {
+ print("Hotline: Merging category into existing category at \(lookupPath)")
+
+ existingCategoryInfo.count = newsCategoryInfo.count
+ existingCategoryInfo.name = newsCategoryInfo.name
+ existingCategoryInfo.path = newsCategoryInfo.path
+ existingCategoryInfo.categoryID = newsCategoryInfo.categoryID
+ newsCategoryInfo = existingCategoryInfo
+ }
+ else {
+ print("Hotline: New category added at \(lookupPath)")
+ self?.newsLookup[lookupPath] = newsCategoryInfo
+ }
}
- continuation.resume(returning: newCategories)
+ newCategoryInfos.append(newsCategoryInfo)
+ }
+
+ if let parent = parentNewsGroup {
+ parent.children = newCategoryInfos
+ }
+ else if path.isEmpty {
+ self?.newsLoaded = true
+ self?.news = newCategoryInfos
}
+
+ continuation.resume()
}
}
else {
- self?.client.sendGetNewsArticles(path: path) { [weak self] articles in
-// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
- print("GENERATING NEWS")
+ print("Hotline: Requesting articles at: /\(path.joined(separator: "/"))")
+
+ self?.client.sendGetNewsArticles(path: path) { @MainActor [weak self] articles in
+ print("Hotline: Organizing news at \(path.joined(separator: "/"))")
- var newArticles: [NewsInfo] = []
- for article in articles {
- newArticles.append(NewsInfo(hotlineNewsArticle: article))
- }
+ // Create info for each article returned.
+ var newArticleInfos: [NewsInfo] = []
- let organizedNewsArticles: [NewsInfo] = self?.organizeNewsList(newArticles) ?? []
-
- DispatchQueue.main.async {
- if let parent = existingNewsItem {
- print("UNDER PARENT:", parent.name)
- parent.children = organizedNewsArticles
-
- print(parent.children)
- }
- else if path.isEmpty {
- self?.news = organizedNewsArticles
+ for article in articles {
+ var newsArticleInfo = NewsInfo(hotlineNewsArticle: article)
+
+ if let lookupPath = newsArticleInfo.lookupPath {
+ // Merge returned category info with existing category info.
+ if let existingArticleInfo = self?.newsLookup[lookupPath] {
+ print("Hotline: Merging article into existing article at \(lookupPath)")
+
+ existingArticleInfo.count = newsArticleInfo.count
+ existingArticleInfo.name = newsArticleInfo.name
+ existingArticleInfo.path = newsArticleInfo.path
+ existingArticleInfo.articleUsername = newsArticleInfo.articleUsername
+ existingArticleInfo.articleDate = newsArticleInfo.articleDate
+ existingArticleInfo.articleFlavors = newsArticleInfo.articleFlavors
+ existingArticleInfo.articleID = newsArticleInfo.articleID
+ newsArticleInfo = existingArticleInfo
+ }
+ else {
+ print("Hotline: New article added at \(lookupPath)")
+ self?.newsLookup[lookupPath] = newsArticleInfo
+ }
}
- continuation.resume(returning: organizedNewsArticles)
+ newArticleInfos.append(newsArticleInfo)
+ }
+
+ let organizedNewsArticles: [NewsInfo] = self?.organizeNewsArticles(newArticleInfos) ?? []
+ if let parent = parentNewsGroup {
+ parent.children = organizedNewsArticles
}
+
+ continuation.resume()
}
}
}
}
- func organizeNewsList(_ news: [NewsInfo]) -> [NewsInfo] {
- var articleMap: [UInt: NewsInfo] = [:]
-
- // Create lookup table of each news item.
- for article in news {
- if let articleID = article.articleID {
- articleMap[articleID] = article
- }
- }
-
+ func organizeNewsArticles(_ flatArticles: [NewsInfo]) -> [NewsInfo] {
// Place articles under their parent.
var organized: [NewsInfo] = []
- for article in news {
- if let parentID = article.parentID,
- parentID != 0,
- let parentArticle = articleMap[parentID] {
- article.expanded = true
- parentArticle.children.append(article)
+ for article in flatArticles {
+ if let parentLookupPath = article.parentArticleLookupPath,
+ let parentArticle = self.newsLookup[parentLookupPath] {
+// article.expanded = true
+ if parentArticle.children.firstIndex(of: article) == nil {
+ article.expanded = true
+ parentArticle.children.append(article)
+ }
}
else {
organized.append(article)
@@ -398,30 +421,51 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
return organized
}
- @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] {
+ @MainActor func postNewsArticle(title: String, body: String, at path: [String], parentID: UInt32 = 0) async -> Bool {
return await withCheckedContinuation { [weak self] continuation in
- self?.client.sendGetNewsCategories(path: path) { [weak self] categories in
- let parentNews = self?.findNews(in: self?.news ?? [], at: path)
-
- var newCategories: [NewsInfo] = []
- for category in categories {
- newCategories.append(NewsInfo(hotlineNewsCategory: category))
- }
-
- DispatchQueue.main.async {
- if let parent = parentNews {
- parent.children = newCategories
- }
- else if path.isEmpty {
- self?.news = newCategories
- }
-
- continuation.resume(returning: newCategories)
- }
+ guard let client = self?.client else {
+ continuation.resume(returning: false)
+ return
}
+
+ client.postNewsArticle(title: title, text: body, path: path, parentID: parentID, callback: { success in
+ print("Hotline: News article posted? \(success)")
+ continuation.resume(returning: success)
+ })
}
}
+// @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] {
+// return await withCheckedContinuation { [weak self] continuation in
+// guard let client = self?.client else {
+// continuation.resume(returning: [])
+// return
+// }
+//
+// client.sendGetNewsCategories(path: path) { [weak self] categories in
+// let parentNews = self?.findNews(in: self?.news ?? [], at: path)
+//
+// var newCategories: [NewsInfo] = []
+// for category in categories {
+// let categoryInfo: NewsInfo = NewsInfo(hotlineNewsCategory: category)
+// newCategories.append(categoryInfo)
+// self?.newsLookup[categoryInfo.path.joined(separator: "/")] = categoryInfo
+// }
+//
+// DispatchQueue.main.async {
+// if let parent = parentNews {
+// parent.children = newCategories
+// }
+// else if path.isEmpty {
+// self?.news = newCategories
+// }
+//
+// continuation.resume(returning: newCategories)
+// }
+// }
+// }
+// }
+
@MainActor func getArticles(at path: [String]) async -> [NewsInfo] {
return await withCheckedContinuation { [weak self] continuation in
self?.client.sendGetNewsArticles(path: path) { articles in
@@ -953,9 +997,7 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
}
private func findNews(in newsToSearch: [NewsInfo], at path: [String]) -> NewsInfo? {
- guard !path.isEmpty, !newsToSearch.isEmpty else { return nil }
-
- let currentName = path[0]
+ guard !path.isEmpty, !newsToSearch.isEmpty, let currentName = path.first else { return nil }
for news in newsToSearch {
if news.name == currentName {
@@ -971,4 +1013,18 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileClientDelegate {
return nil
}
+
+ private func findNewsArticle(id articleID: UInt32, at path: [String]) -> NewsInfo? {
+ guard let parent = self.findNews(in: self.news, at: path), !parent.children.isEmpty else {
+ return nil
+ }
+
+ return parent.children.first { child in
+ guard let childArticleID = child.articleID else {
+ return false
+ }
+
+ return child.type == .article && child.articleID == childArticleID
+ }
+ }
}
diff --git a/Hotline/Models/NewsInfo.swift b/Hotline/Models/NewsInfo.swift
index eecd550..6ee28b8 100644
--- a/Hotline/Models/NewsInfo.swift
+++ b/Hotline/Models/NewsInfo.swift
@@ -9,15 +9,15 @@ enum NewsInfoType {
@Observable class NewsInfo: Identifiable, Hashable {
let id: UUID = UUID()
- let name: String
- let count: UInt
+ var name: String
+ var count: UInt
let type: NewsInfoType
- let categoryID: UUID?
- let articleID: UInt?
- let parentID: UInt?
+ var categoryID: UUID?
+ var articleID: UInt?
+ var parentID: UInt?
- let path: [String]
+ var path: [String]
var expanded: Bool = false
var children: [NewsInfo] = []
@@ -30,6 +30,38 @@ enum NewsInfoType {
self.type == .bundle || self.type == .category || self.children.count > 0
}
+ var lookupPath: String? {
+ switch self.type {
+ case .bundle, .category:
+ return "/\(self.path.joined(separator: "/"))"
+ case .article:
+ guard let aid = self.articleID else {
+ return nil
+ }
+// if let pid = self.parentID, pid != 0 {
+// return "/\(self.path.joined(separator: "/"))/\(pid)/\(aid)"
+// }
+ return "/\(self.path.joined(separator: "/"))/\(aid)"
+ }
+ }
+
+ var parentArticleLookupPath: String? {
+ switch self.type {
+ case .bundle, .category:
+// if self.path.count <= 1 {
+// return "/"
+// }
+// let parentPath = self.path[0..<self.path.count-1]
+// return "/\(parentPath.joined(separator: "/"))"
+ return nil
+ case .article:
+ guard let pid = self.parentID, pid != 0 else {
+ return nil
+ }
+ return "/\(self.path.joined(separator: "/"))/\(pid)"
+ }
+ }
+
init(hotlineNewsCategory: HotlineNewsCategory) {
self.categoryID = hotlineNewsCategory.id
self.articleID = nil
diff --git a/Hotline/Utility/FoundationExtensions.swift b/Hotline/Utility/FoundationExtensions.swift
index 3a325be..ebd179a 100644
--- a/Hotline/Utility/FoundationExtensions.swift
+++ b/Hotline/Utility/FoundationExtensions.swift
@@ -7,6 +7,13 @@ enum Endianness {
}
extension String {
+ func replyToString() -> String {
+ if self.range(of: "^Re:", options: [.regularExpression, .caseInsensitive]) != nil {
+ return String(self)
+ }
+ return "Re: \(self)"
+ }
+
func convertToAttributedStringWithLinks() -> AttributedString {
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self)
let matches = self.ranges(of: RegularExpressions.relaxedLink)
diff --git a/Hotline/iOS/NewsView.swift b/Hotline/iOS/NewsView.swift
index 6d3ad45..460dcd3 100644
--- a/Hotline/iOS/NewsView.swift
+++ b/Hotline/iOS/NewsView.swift
@@ -220,7 +220,7 @@ struct NewsView: View {
}
.task {
if !fetched {
- let _ = await model.getNewsList()
+ await model.getNewsList()
fetched = true
}
}
diff --git a/Hotline/macOS/AboutView.swift b/Hotline/macOS/AboutView.swift
index 1cf7dc2..baafcf8 100644
--- a/Hotline/macOS/AboutView.swift
+++ b/Hotline/macOS/AboutView.swift
@@ -89,20 +89,21 @@ struct AboutView: View {
VStack(alignment: .leading, spacing: 4) {
Link(destination: URL(string: "https://github.com/mierau/hotline")!) {
- HStack(alignment: .center) {
- Text("GitHub Contributors")
+ HStack(alignment: .center, spacing: 4) {
+ Text("Contributors")
.lineLimit(1)
.font(.system(size: 16))
.fontWeight(.semibold)
.foregroundStyle(.black)
- .opacity(0.5)
-
+ .opacity(0.75)
+
Image(systemName: "arrow.forward.circle.fill")
.resizable()
+ .fontWeight(.bold)
.scaledToFit()
- .frame(width: 16, height: 16)
+ .frame(width: 12, height: 12)
.foregroundStyle(.black)
- .opacity(0.5)
+ .opacity(0.75)
}
}
.padding(.top, 24)
diff --git a/Hotline/macOS/ChatView.swift b/Hotline/macOS/ChatView.swift
index 0875106..62bab69 100644
--- a/Hotline/macOS/ChatView.swift
+++ b/Hotline/macOS/ChatView.swift
@@ -49,12 +49,8 @@ struct ChatView: View {
}
// MARK: Server Message
else if msg.type == .server {
- HStack {
- Spacer()
- ServerMessageView(message: msg.text)
- Spacer()
- }
- .padding(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
+ ServerMessageView(message: msg.text)
+ .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
}
// MARK: Status
else if msg.type == .status {
diff --git a/Hotline/macOS/FileDetailsView.swift b/Hotline/macOS/FileDetailsView.swift
index d3c10e8..aa7ab09 100644
--- a/Hotline/macOS/FileDetailsView.swift
+++ b/Hotline/macOS/FileDetailsView.swift
@@ -15,7 +15,7 @@ struct FileDetailsView: View {
Form {
HStack(alignment: .center){
FileIconView(filename: fd.name)
- .frame(width: 16, height: 16)
+ .frame(width: 32, height: 32)
TextField("", text: $filename)
.disabled(!self.canRename())
}
diff --git a/Hotline/macOS/FilePreviewImageView.swift b/Hotline/macOS/FilePreviewImageView.swift
index ae9a825..b8ad37b 100644
--- a/Hotline/macOS/FilePreviewImageView.swift
+++ b/Hotline/macOS/FilePreviewImageView.swift
@@ -80,9 +80,9 @@ struct FilePreviewImageView: View {
Button {
let _ = preview?.data?.saveAsFileToDownloads(filename: info.name)
} label: {
- Label("Save Image...", systemImage: "photo.badge.arrow.down")
+ Label("Download Image...", systemImage: "arrow.down")
}
- .help("Save Image")
+ .help("Download Image")
}
ToolbarItem(placement: .primaryAction) {
diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift
index e3f5118..a225549 100644
--- a/Hotline/macOS/FilesView.swift
+++ b/Hotline/macOS/FilesView.swift
@@ -192,18 +192,18 @@ struct FilesView: View {
}
}
.toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button {
- } label: {
- Label("Delete", systemImage: "trash")
- }
- .help("Delete")
- .disabled(true)
- }
+// ToolbarItem(placement: .primaryAction) {
+// Button {
+// } label: {
+// Label("Delete", systemImage: "trash")
+// }
+// .help("Delete")
+// .disabled(true)
+// }
ToolbarItem(placement: .primaryAction) {
Button {
- if let s = selection, !s.isFolder {
+ if let s = selection, s.isPreviewable {
model.previewFile(s.name, path: s.path) { info in
if let info = info {
openPreviewWindow(info)
diff --git a/Hotline/macOS/HotlinePanelView.swift b/Hotline/macOS/HotlinePanelView.swift
index d486761..69d00aa 100644
--- a/Hotline/macOS/HotlinePanelView.swift
+++ b/Hotline/macOS/HotlinePanelView.swift
@@ -4,9 +4,6 @@ struct HotlinePanelView: View {
@Environment(\.openWindow) var openWindow
@Environment(\.colorScheme) var colorScheme
- @FocusedValue(\.activeHotlineModel) private var activeHotline: Hotline?
- @FocusedValue(\.activeServerState) private var activeServerState: ServerState?
-
var body: some View {
VStack(spacing: 0) {
Image(nsImage: ApplicationState.shared.activeServerBanner ?? NSImage(named: "Default Banner")!)
diff --git a/Hotline/macOS/NewsEditorView.swift b/Hotline/macOS/NewsEditorView.swift
index 18c4a0d..ff33281 100644
--- a/Hotline/macOS/NewsEditorView.swift
+++ b/Hotline/macOS/NewsEditorView.swift
@@ -1,5 +1,9 @@
import SwiftUI
-import UniformTypeIdentifiers
+
+private enum FocusFields {
+ case title
+ case body
+}
struct NewsEditorView: View {
@Environment(\.controlActiveState) private var controlActiveState
@@ -7,94 +11,145 @@ struct NewsEditorView: View {
@Environment(\.dismiss) private var dismiss
@Environment(Hotline.self) private var model: Hotline
-// @Binding var article: NewsArticle?
+ let editorTitle: String
+ let isReply: Bool
+ let path: [String]
+ let parentID: UInt32
+
@State var title: String = ""
- @State var text: String = ""
+ @State private var text: String = ""
+ @State private var sending: Bool = false
+
+ @FocusState private var focusedField: FocusFields?
+
+ func sendArticle() async -> Bool {
+ sending = true
+
+ let success = await model.postNewsArticle(title: title, body: text, at: path, parentID: parentID)
+ if success {
+ await model.getNewsList(at: path)
+ }
+
+ sending = false
+
+ return success
+ }
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()
-
+ VStack(alignment: .leading, spacing: 0) {
+ HStack(alignment: .center, spacing: 0) {
+ Button {
+ dismiss()
+ } label: {
+ Image(systemName: "xmark")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 14, height: 14)
+ .opacity(0.5)
+ }
+ .buttonStyle(.plain)
+ .frame(width: 16, height: 16)
+
+ Spacer()
+
+ if !isReply {
+ Image("News Category")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16)
+ .padding(.trailing, 6)
+ }
+
+ Text(editorTitle)
+ .fontWeight(.semibold)
+ .lineLimit(1)
+ .truncationMode(.middle)
+
+ Spacer()
+
+ if sending {
+ ProgressView()
+ .controlSize(.small)
+ .frame(width: 22, height: 22)
+ }
+ else {
Button {
+ Task {
+ if await sendArticle() {
+ dismiss()
+ }
+ }
} label: {
- Image(systemName: "paperplane")
+ Image(systemName: "arrow.up.circle.fill")
.resizable()
+ .renderingMode(.template)
.scaledToFit()
+ .foregroundColor((title.isEmpty || text.isEmpty) ? .secondary : .accentColor)
}
.buttonStyle(.plain)
- .frame(width: 16, height: 16)
- .padding()
+ .frame(width: 22, height: 22)
+ .help("Post to Newsgroup")
+ .disabled(title.isEmpty || text.isEmpty)
}
+ }
+ .frame(maxWidth: .infinity)
+ .padding([.leading, .top, .trailing])
+
+ TextField("Title", text: $title, axis: .vertical)
+ .textFieldStyle(.plain)
+ .lineLimit(3)
+ .padding()
+ .focusEffectDisabled()
+ .fontWeight(.semibold)
.frame(maxWidth: .infinity)
- TextField("Title", text: $title, axis: .vertical)
- .textFieldStyle(.plain)
+ .border(Color.pink, width: 0)
+ .background(.tertiary.opacity(0.2))
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ .padding()
+ .focused($focusedField, equals: .title)
+
+ Divider()
+
+ TextEditor(text: $text)
+ .textEditorStyle(.plain)
+ .font(.system(size: 14, design: .monospaced))
+ .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)
+ .clipped()
+ .focused($focusedField, equals: .body)
+
+ HStack(alignment: .center) {
+ Spacer()
+
+ Text(String("**bold** _italics_ [link name](url) ![image name](url)"))
+ .foregroundStyle(.secondary)
+ .font(.caption)
+ .fontDesign(.monospaced)
+ .lineLimit(1)
+ .truncationMode(.middle)
.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)
+
+ Spacer()
}
+ .frame(maxWidth: .infinity)
+ .background(.tertiary.opacity(0.15))
}
.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
-// }
+ if !title.isEmpty {
+ focusedField = .body
+ }
}
.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 8df741f..ed5d969 100644
--- a/Hotline/macOS/NewsView.swift
+++ b/Hotline/macOS/NewsView.swift
@@ -12,6 +12,8 @@ struct NewsView: View {
@State private var splitHidden = SideHolder(.bottom)
@State private var splitFraction = FractionHolder.usingUserDefaults(0.25, key: "News Split Fraction")
@State private var editorOpen: Bool = false
+ @State private var replyOpen: Bool = false
+ @State private var loading: Bool = true
var body: some View {
Group {
@@ -63,45 +65,82 @@ struct NewsView: View {
}
.task {
if !model.newsLoaded {
- let _ = await model.getNewsList()
+ await model.getNewsList()
+ loading = false
}
}
}
}
.sheet(isPresented: $editorOpen) {
- print("Sheet dismissed!")
} content: {
- NewsEditorView()
- }
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button {
-
- } label: {
- Image(systemName: "trash")
+ if let selection = selection {
+ switch selection.type {
+ case .article, .category:
+ NewsEditorView(editorTitle: selection.path.last ?? "New Post", isReply: false, path: selection.path, parentID: 0)
+ default:
+ EmptyView()
}
}
-
+ else {
+ EmptyView()
+ }
+ }
+ .sheet(isPresented: $replyOpen) {
+ } content: {
+ if let selection = selection, selection.type == .article {
+ NewsEditorView(editorTitle: "Reply to \(selection.articleUsername ?? "Post")", isReply: true, path: selection.path, parentID: UInt32(selection.articleID!), title: selection.name.replyToString())
+ }
+ else {
+ EmptyView()
+ }
+ }
+ .toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
if selection?.type == .category || selection?.type == .article {
editorOpen = true
-// openWindow(id: "news-editor", value: NewsArticle(parentID: nil, path: selection.path, title: "", body: ""))
}
} label: {
Image(systemName: "square.and.pencil")
}
+ .help("New Post")
.disabled(selection?.type != .category && selection?.type != .article)
}
ToolbarItem(placement: .primaryAction) {
Button {
-
+ if selection?.type == .article {
+ replyOpen = true
+ }
} label: {
Image(systemName: "arrowshape.turn.up.left")
}
+ .help("Reply to Post")
.disabled(selection?.type != .article)
}
+
+ ToolbarItem(placement: .primaryAction) {
+ Button {
+ if let selectionPath = selection?.path {
+ Task {
+ loading = true
+ await model.getNewsList(at: selectionPath)
+ loading = false
+ }
+ }
+ else {
+ Task {
+ loading = true
+ await model.getNewsList()
+ loading = false
+ }
+ }
+ } label: {
+ Image(systemName: "arrow.clockwise")
+ }
+ .help("Reload Newsgroup")
+ .disabled(loading)
+ }
}
}
diff --git a/Hotline/macOS/ServerMessageView.swift b/Hotline/macOS/ServerMessageView.swift
index 742703f..8413a87 100644
--- a/Hotline/macOS/ServerMessageView.swift
+++ b/Hotline/macOS/ServerMessageView.swift
@@ -15,9 +15,15 @@ struct ServerMessageView: View {
.lineSpacing(4)
.multilineTextAlignment(.leading)
.textSelection(.enabled)
+ Spacer()
}
.padding()
+ .frame(maxWidth: .infinity)
+#if os(iOS)
.background(Color("Agreement Background"))
+#elseif os(macOS)
+ .background(VisualEffectView(material: .titlebar, blendingMode: .withinWindow))
+#endif
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}