diff options
| author | Dustin Mierau <dustin@mierau.me> | 2025-11-07 10:39:48 -0800 |
|---|---|---|
| committer | Dustin Mierau <dustin@mierau.me> | 2025-11-07 10:39:48 -0800 |
| commit | 39f51fd902bb34272c78ffdfb872c22095382f70 (patch) | |
| tree | fee51effb6431aeb5464931052ecdd0df32c6792 /Hotline/macOS | |
| parent | 87f08cf60a5d7c1cf618463916cbac4dab88e0f8 (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.swift | 121 | ||||
| -rw-r--r-- | Hotline/macOS/Files/FilesView.swift | 272 | ||||
| -rw-r--r-- | Hotline/macOS/News/NewsView.swift | 42 |
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 { |