aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-11-07 10:39:48 -0800
committerDustin Mierau <dustin@mierau.me>2025-11-07 10:39:48 -0800
commit39f51fd902bb34272c78ffdfb872c22095382f70 (patch)
treefee51effb6431aeb5464931052ecdd0df32c6792 /Hotline/macOS
parent87f08cf60a5d7c1cf618463916cbac4dab88e0f8 (diff)
Further cleanup of previous refactor. Removing old view model and NetSocket. Added some empty states for newsgroups and message board.
Diffstat (limited to 'Hotline/macOS')
-rw-r--r--Hotline/macOS/Board/MessageBoardView.swift121
-rw-r--r--Hotline/macOS/Files/FilesView.swift272
-rw-r--r--Hotline/macOS/News/NewsView.swift42
3 files changed, 213 insertions, 222 deletions
diff --git a/Hotline/macOS/Board/MessageBoardView.swift b/Hotline/macOS/Board/MessageBoardView.swift
index 8788d66..710ff20 100644
--- a/Hotline/macOS/Board/MessageBoardView.swift
+++ b/Hotline/macOS/Board/MessageBoardView.swift
@@ -8,91 +8,82 @@ struct MessageBoardView: View {
var body: some View {
NavigationStack {
- if model.access?.contains(.canReadMessageBoard) != false {
- ScrollView {
- LazyVStack(alignment: .leading) {
- ForEach(model.messageBoard, id: \.self) { msg in
- Text(LocalizedStringKey(msg))
- .tint(Color("Link Color"))
- .lineLimit(100)
- .lineSpacing(4)
- .padding()
- .textSelection(.enabled)
- Divider()
- }
- }
- Spacer()
+ if self.model.access?.contains(.canReadMessageBoard) != false {
+ if self.model.messageBoardLoaded && self.model.messageBoard.isEmpty {
+ self.emptyBoardView
}
- .task {
- if !model.messageBoardLoaded {
- let _ = try? await model.getMessageBoard()
- }
+ else {
+ self.messageBoardView
}
- .overlay {
- if !model.messageBoardLoaded {
- VStack {
- ProgressView()
- .controlSize(.large)
- }
- .frame(maxWidth: .infinity)
- }
- }
- .background(Color(nsColor: .textBackgroundColor))
}
else {
- ZStack(alignment: .center) {
- Text("No Message Board")
- .font(.title)
- .multilineTextAlignment(.center)
- .foregroundStyle(.secondary)
- .padding()
- }
- .frame(maxWidth: .infinity)
+ self.disabledBoardView
}
}
.sheet(isPresented: $composerDisplayed) {
MessageBoardEditorView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(idealWidth: 450, idealHeight: 350)
-// RichTextEditor(text: $composerText)
-// .richEditorFont(NSFont.systemFont(ofSize: 16.0))
-// .richEditorAutomaticDashSubstitution(false)
-// .richEditorAutomaticQuoteSubstitution(false)
-// .richEditorAutomaticSpellingCorrection(false)
-// .background(Color(nsColor: .textBackgroundColor))
-// .frame(maxWidth: .infinity, maxHeight: .infinity)
-// .frame(idealWidth: 450, idealHeight: 350)
-// .toolbar {
-// ToolbarItem(placement: .cancellationAction) {
-// Button("Cancel") {
-// composerDisplayed.toggle()
-// }
-// }
-//
-// ToolbarItem(placement: .primaryAction) {
-// Button("Post") {
-// composerDisplayed.toggle()
-// let text = composerText
-// composerText = ""
-// model.postToMessageBoard(text: text)
-// Task {
-// await model.getMessageBoard()
-// }
-// }
-// }
-// }
}
.toolbar {
ToolbarItem(placement:.primaryAction) {
Button {
- composerDisplayed.toggle()
+ self.composerDisplayed.toggle()
} label: {
Image(systemName: "square.and.pencil")
}
- .disabled(model.access?.contains(.canPostMessageBoard) == false)
+ .disabled(self.model.access?.contains(.canPostMessageBoard) == false)
.help("Post to Message Board")
}
}
+ .task {
+ if !self.model.messageBoardLoaded {
+ let _ = try? await self.model.getMessageBoard()
+ }
+ }
+ }
+
+ private var disabledBoardView: some View {
+ ContentUnavailableView {
+ Label("Message Board Disabled", systemImage: "quote.bubble")
+ } description: {
+ Text("This server has turned off the message board")
+ }
+ }
+
+ private var emptyBoardView: some View {
+ ContentUnavailableView {
+ Label("No Posts", systemImage: "quote.bubble")
+ } description: {
+ Text("Message board posts will appear here")
+ }
+ }
+
+ private var messageBoardView: some View {
+ ScrollView {
+ LazyVStack(alignment: .leading) {
+ ForEach(self.model.messageBoard, id: \.self) { msg in
+ Text(LocalizedStringKey(msg))
+ .tint(Color("Link Color"))
+ .lineLimit(100)
+ .lineSpacing(4)
+ .padding()
+ .textSelection(.enabled)
+ Divider()
+ }
+ }
+ Spacer()
+ }
+ .overlay {
+ if !self.model.messageBoardLoaded {
+ VStack {
+ ProgressView()
+ .controlSize(.large)
+ }
+ .frame(maxWidth: .infinity)
+ }
+ }
+ .background(Color(nsColor: .textBackgroundColor))
}
}
diff --git a/Hotline/macOS/Files/FilesView.swift b/Hotline/macOS/Files/FilesView.swift
index b499fe6..d4ba5c8 100644
--- a/Hotline/macOS/Files/FilesView.swift
+++ b/Hotline/macOS/Files/FilesView.swift
@@ -16,142 +16,6 @@ struct FilesView: View {
@State private var searchText: String = ""
@State private var isSearching: Bool = false
@State private var dragOver: Bool = false
-
- private var isShowingSearchResults: Bool {
- switch model.fileSearchStatus {
- case .idle:
- return !model.fileSearchResults.isEmpty
- case .cancelled(_):
- return !model.fileSearchResults.isEmpty
- default:
- return true
- }
- }
-
- private var displayedFiles: [FileInfo] {
- isShowingSearchResults ? model.fileSearchResults : model.files
- }
-
- private var searchStatusMessage: String? {
- switch model.fileSearchStatus {
- case .searching(let processed, _):
- let scanned = processed == 1 ? "folder" : "folders"
- return "Searched \(processed) \(scanned)..."
- case .completed(let processed):
- let count = model.fileSearchResults.count
- let folderWord = processed == 1 ? "folder" : "folders"
- if count == 0 {
- return "No files found in \(processed) \(folderWord)"
- }
- return "\(count) file\(count == 1 ? "" : "s") found in \(processed) \(folderWord)"
- case .cancelled(_):
- if model.fileSearchResults.isEmpty {
- return nil
- }
- return "Search cancelled"
- case .failed(let message):
- return "Search failed: \(message)"
- case .idle:
- return nil
- }
- }
-
- private var searchStatusPath: String? {
- guard let path = model.fileSearchCurrentPath else {
- return nil
- }
- if path.isEmpty {
- return "/"
- }
- return path.joined(separator: "/")
- }
-
- private func openPreviewWindow(_ previewInfo: PreviewFileInfo) {
- switch previewInfo.previewType {
- case .image:
- openWindow(id: "preview-quicklook", value: previewInfo)
- case .text:
- openWindow(id: "preview-quicklook", value: previewInfo)
- case .unknown:
- openWindow(id: "preview-quicklook", value: previewInfo)
- return
- }
- }
-
- @MainActor private func getFileInfo(_ file: FileInfo) {
- Task {
- if let fileInfo = try? await model.getFileDetails(file.name, path: file.path) {
- Task { @MainActor in
- self.fileDetails = fileInfo
- }
- }
- }
- }
-
- @MainActor private func downloadFile(_ file: FileInfo) {
- if file.isFolder {
- model.downloadFolderNew(file.name, path: file.path)
- }
- else {
- model.downloadFileNew(file.name, path: file.path)
- }
- }
-
- @MainActor private func uploadFile(file fileURL: URL, to path: [String]) {
- model.uploadFile(url: fileURL, path: path) { info in
- Task {
- // Refresh file listing to display newly uploaded file.
- let _ = try? await model.getFileList(path: path)
- }
- }
- }
-
- @MainActor private func upload(file fileURL: URL, to path: [String]) {
- var fileIsDirectory: ObjCBool = false
- guard FileManager.default.fileExists(atPath: fileURL.path(percentEncoded: false), isDirectory: &fileIsDirectory) else {
- return
- }
-
- if fileIsDirectory.boolValue {
- self.model.uploadFolder(url: fileURL, path: path, complete: { info in
- Task {
- // Refresh file listing to display newly uploaded file.
- try? await model.getFileList(path: path)
- }
- })
- }
- else {
- self.model.uploadFile(url: fileURL, path: path) { info in
- Task {
- // Refresh file listing to display newly uploaded file.
- try? await model.getFileList(path: path)
- }
- }
- }
- }
-
- @MainActor private func previewFile(_ file: FileInfo) {
- guard file.isPreviewable else {
- return
- }
-
- model.previewFile(file.name, path: file.path) { info in
- if let info = info {
- openPreviewWindow(info)
- }
- }
- }
-
- private func deleteFile(_ file: FileInfo) async {
- var parentPath: [String] = []
- if file.path.count > 1 {
- parentPath = Array(file.path[0..<file.path.count-1])
- }
-
- if (try? await model.deleteFile(file.name, path: file.path)) == true {
- let _ = try? await model.getFileList(path: parentPath)
- }
- }
var body: some View {
NavigationStack {
@@ -459,6 +323,142 @@ struct FilesView: View {
}
}
}
+
+ private var isShowingSearchResults: Bool {
+ switch model.fileSearchStatus {
+ case .idle:
+ return !model.fileSearchResults.isEmpty
+ case .cancelled(_):
+ return !model.fileSearchResults.isEmpty
+ default:
+ return true
+ }
+ }
+
+ private var displayedFiles: [FileInfo] {
+ isShowingSearchResults ? model.fileSearchResults : model.files
+ }
+
+ private var searchStatusMessage: String? {
+ switch model.fileSearchStatus {
+ case .searching(let processed, _):
+ let scanned = processed == 1 ? "folder" : "folders"
+ return "Searched \(processed) \(scanned)..."
+ case .completed(let processed):
+ let count = model.fileSearchResults.count
+ let folderWord = processed == 1 ? "folder" : "folders"
+ if count == 0 {
+ return "No files found in \(processed) \(folderWord)"
+ }
+ return "\(count) file\(count == 1 ? "" : "s") found in \(processed) \(folderWord)"
+ case .cancelled(_):
+ if model.fileSearchResults.isEmpty {
+ return nil
+ }
+ return "Search cancelled"
+ case .failed(let message):
+ return "Search failed: \(message)"
+ case .idle:
+ return nil
+ }
+ }
+
+ private var searchStatusPath: String? {
+ guard let path = model.fileSearchCurrentPath else {
+ return nil
+ }
+ if path.isEmpty {
+ return "/"
+ }
+ return path.joined(separator: "/")
+ }
+
+ private func openPreviewWindow(_ previewInfo: PreviewFileInfo) {
+ switch previewInfo.previewType {
+ case .image:
+ openWindow(id: "preview-quicklook", value: previewInfo)
+ case .text:
+ openWindow(id: "preview-quicklook", value: previewInfo)
+ case .unknown:
+ openWindow(id: "preview-quicklook", value: previewInfo)
+ return
+ }
+ }
+
+ @MainActor private func getFileInfo(_ file: FileInfo) {
+ Task {
+ if let fileInfo = try? await model.getFileDetails(file.name, path: file.path) {
+ Task { @MainActor in
+ self.fileDetails = fileInfo
+ }
+ }
+ }
+ }
+
+ @MainActor private func downloadFile(_ file: FileInfo) {
+ if file.isFolder {
+ model.downloadFolderNew(file.name, path: file.path)
+ }
+ else {
+ model.downloadFileNew(file.name, path: file.path)
+ }
+ }
+
+ @MainActor private func uploadFile(file fileURL: URL, to path: [String]) {
+ model.uploadFile(url: fileURL, path: path) { info in
+ Task {
+ // Refresh file listing to display newly uploaded file.
+ let _ = try? await model.getFileList(path: path)
+ }
+ }
+ }
+
+ @MainActor private func upload(file fileURL: URL, to path: [String]) {
+ var fileIsDirectory: ObjCBool = false
+ guard FileManager.default.fileExists(atPath: fileURL.path(percentEncoded: false), isDirectory: &fileIsDirectory) else {
+ return
+ }
+
+ if fileIsDirectory.boolValue {
+ self.model.uploadFolder(url: fileURL, path: path, complete: { info in
+ Task {
+ // Refresh file listing to display newly uploaded file.
+ try? await model.getFileList(path: path)
+ }
+ })
+ }
+ else {
+ self.model.uploadFile(url: fileURL, path: path) { info in
+ Task {
+ // Refresh file listing to display newly uploaded file.
+ try? await model.getFileList(path: path)
+ }
+ }
+ }
+ }
+
+ @MainActor private func previewFile(_ file: FileInfo) {
+ guard file.isPreviewable else {
+ return
+ }
+
+ model.previewFile(file.name, path: file.path) { info in
+ if let info = info {
+ openPreviewWindow(info)
+ }
+ }
+ }
+
+ private func deleteFile(_ file: FileInfo) async {
+ var parentPath: [String] = []
+ if file.path.count > 1 {
+ parentPath = Array(file.path[0..<file.path.count-1])
+ }
+
+ if (try? await model.deleteFile(file.name, path: file.path)) == true {
+ let _ = try? await model.getFileList(path: parentPath)
+ }
+ }
}
#Preview {
diff --git a/Hotline/macOS/News/NewsView.swift b/Hotline/macOS/News/NewsView.swift
index dfc5ab2..a07a441 100644
--- a/Hotline/macOS/News/NewsView.swift
+++ b/Hotline/macOS/News/NewsView.swift
@@ -18,16 +18,7 @@ struct NewsView: View {
var body: some View {
Group {
if model.serverVersion < 151 {
- VStack {
- Text("No News")
- .bold()
- .foregroundStyle(.secondary)
- .font(.title3)
- Text("This server has news turned off.")
- .foregroundStyle(.tertiary)
- .font(.system(size: 13))
- }
- .padding()
+ disabledNewsView
}
else {
NavigationStack {
@@ -37,14 +28,7 @@ struct NewsView: View {
loadingIndicator
}
else if model.news.isEmpty {
- ZStack(alignment: .center) {
- Text("No News")
- .font(.title)
- .multilineTextAlignment(.center)
- .foregroundStyle(.secondary)
- .padding()
- }
- .frame(maxWidth: .infinity)
+ emptyNewsView
}
else {
newsBrowser
@@ -142,7 +126,23 @@ struct NewsView: View {
}
}
- var newsBrowser: some View {
+ private var disabledNewsView: some View {
+ ContentUnavailableView {
+ Label("No News", systemImage: "newspaper")
+ } description: {
+ Text("This server has turned off newsgroups")
+ }
+ }
+
+ private var emptyNewsView: some View {
+ ContentUnavailableView {
+ Label("No News", systemImage: "newspaper")
+ } description: {
+ Text("This server has no newsgroups")
+ }
+ }
+
+ private var newsBrowser: some View {
List(model.news, id: \.self, selection: $selection) { newsItem in
NewsItemView(news: newsItem, depth: 0).tag(newsItem.id)
}
@@ -215,7 +215,7 @@ struct NewsView: View {
}
}
- var loadingIndicator: some View {
+ private var loadingIndicator: some View {
VStack {
HStack {
ProgressView {
@@ -227,7 +227,7 @@ struct NewsView: View {
.frame(maxWidth: .infinity)
}
- var articleViewer: some View {
+ private var articleViewer: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
if let selection = selection, selection.type == .article {