aboutsummaryrefslogtreecommitdiff
path: root/Hotline/macOS
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2025-10-22 17:32:59 -0700
committerDustin Mierau <dustin@mierau.me>2025-10-22 17:32:59 -0700
commit46213b6e09b8b23bfa039634ff5af4919c758473 (patch)
tree3ce3e722cc918d42062ec8928db70e88a9785acf /Hotline/macOS
parentc3d0c44698b542a2b46753531bcf3a2c4d264991 (diff)
First pass at file search in files view.
Diffstat (limited to 'Hotline/macOS')
-rw-r--r--Hotline/macOS/FilesView.swift139
-rw-r--r--Hotline/macOS/ServerView.swift11
2 files changed, 139 insertions, 11 deletions
diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift
index f399372..206e0a4 100644
--- a/Hotline/macOS/FilesView.swift
+++ b/Hotline/macOS/FilesView.swift
@@ -213,6 +213,48 @@ struct FilesView: View {
@State private var selection: FileInfo?
@State private var fileDetails: FileDetails?
@State private var uploadFileSelectorDisplayed: Bool = false
+ @State private var searchText: String = ""
+
+ 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 pending):
+ 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(let processed):
+ if model.fileSearchResults.isEmpty {
+ return nil
+ }
+ let count = model.fileSearchResults.count
+ let folderWord = processed == 1 ? "folder" : "folders"
+ return "Search cancelled"
+ case .failed(let message):
+ return "Search failed: \(message)"
+ case .idle:
+ return nil
+ }
+ }
private func openPreviewWindow(_ previewInfo: PreviewFileInfo) {
switch previewInfo.previewType {
@@ -278,7 +320,7 @@ struct FilesView: View {
var body: some View {
NavigationStack {
- List(model.files, id: \.self, selection: $selection) { file in
+ List(displayedFiles, id: \.self, selection: $selection) { file in
if file.isFolder {
FolderView(file: file, depth: 0).tag(file.id)
}
@@ -383,8 +425,9 @@ struct FilesView: View {
.frame(maxWidth: .infinity)
}
}
+ .searchable(text: $searchText, placement: .automatic, prompt: "Search")
.toolbar {
- ToolbarItem(placement: .primaryAction) {
+ ToolbarItemGroup(placement: .automatic) {
Button {
if let selectedFile = selection, selectedFile.isPreviewable {
previewFile(selectedFile)
@@ -394,9 +437,7 @@ struct FilesView: View {
}
.help("Preview")
.disabled(selection == nil || selection?.isPreviewable == false)
- }
-
- ToolbarItem(placement: .primaryAction) {
+
Button {
if let selectedFile = selection {
getFileInfo(selectedFile)
@@ -406,9 +447,7 @@ struct FilesView: View {
}
.help("Get Info")
.disabled(selection == nil)
- }
-
- ToolbarItem(placement: .primaryAction) {
+
Button {
uploadFileSelectorDisplayed = true
} label: {
@@ -416,9 +455,7 @@ struct FilesView: View {
}
.help("Upload")
.disabled(model.access?.contains(.canUploadFiles) != true)
- }
-
- ToolbarItem(placement: .primaryAction) {
+
Button {
if let selectedFile = selection {
downloadFile(selectedFile)
@@ -464,6 +501,86 @@ struct FilesView: View {
print(error)
}
})
+ .onSubmit(of: .search) {
+ let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else {
+ model.cancelFileSearch()
+ return
+ }
+ searchText = trimmed
+ model.startFileSearch(query: trimmed)
+ }
+ .onChange(of: searchText) { _, newValue in
+ if newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+ if isShowingSearchResults {
+ model.cancelFileSearch()
+ }
+ }
+ }
+ .onChange(of: model.fileSearchQuery) { _, newValue in
+ if newValue != searchText {
+ searchText = newValue
+ }
+ }
+ .onAppear {
+ if searchText != model.fileSearchQuery {
+ searchText = model.fileSearchQuery
+ }
+ }
+ .safeAreaInset(edge: .top) {
+ if isShowingSearchResults, let message = searchStatusMessage {
+ HStack {
+ Spacer()
+
+ if case .searching(_, _) = model.fileSearchStatus {
+ ProgressView()
+ .controlSize(.small)
+ .tint(.red)
+ }
+ else if case .completed = model.fileSearchStatus {
+ Image(systemName: "checkmark.circle.fill")
+ .resizable()
+ .symbolRenderingMode(.palette)
+ .foregroundStyle(.white, .fileComplete)
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 16, height: 16)
+ }
+ else if case .failed = model.fileSearchStatus {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .resizable()
+ .symbolRenderingMode(.multicolor)
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 16, height: 16)
+ }
+
+ Text(message)
+ .font(.body)
+ .foregroundStyle(.white)
+
+ Spacer()
+ }
+// .overlay(alignment: .leading) {
+// Button {
+// model.cancelFileSearch(clearResults: true)
+// } label: {
+// Image(systemName: "xmark.circle.fill")
+// .resizable()
+// .scaledToFit()
+// .foregroundStyle(.white)
+// .opacity(0.8)
+// }
+// .buttonStyle(.plain)
+// }
+ .padding(.horizontal, 8)
+ .padding(.leading, 4)
+ .padding(.vertical, 8)
+ .background {
+ Color(nsColor: .controlAccentColor)
+ .clipShape(.capsule(style: .continuous))
+ }
+ .padding(.horizontal, 8)
+ }
+ }
}
}
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index 81c993a..49413fe 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -392,6 +392,16 @@ struct ServerView: View {
ListItemView(icon: menuItem.image, title: menuItem.name, unread: false).tag(menuItem.type)
}
}
+ else if menuItem.type == .files {
+ ListItemView(icon: menuItem.image, title: menuItem.name, unread: false).tag(menuItem.type)
+ .overlay(alignment: .trailing) {
+ if case .searching(_, _) = model.fileSearchStatus {
+ ProgressView()
+ .controlSize(.mini)
+ .padding(.trailing, 4)
+ }
+ }
+ }
else {
ListItemView(icon: menuItem.image, title: menuItem.name, unread: false).tag(menuItem.type)
}
@@ -700,3 +710,4 @@ struct TransferItemView: View {
.help(formattedProgressHelp())
}
}
+