@@ -93,6 +93,7 @@ struct FileListView: View {
93
93
@State private var searchClicked = false
94
94
@State private var searchInProgress = false
95
95
@State private var searchText = " "
96
+ @State private var searchTask : Task < Void , Never > ?
96
97
@State private var searchType : SearchType ?
97
98
@State private var previousPathStack : [ String ] = [ ]
98
99
@@ -142,9 +143,10 @@ struct FileListView: View {
142
143
. submitLabel ( . go)
143
144
. onSubmit {
144
145
guard !searchText. isEmpty else { return }
145
- DispatchQueue . main . async {
146
+ searchTask = Task {
146
147
searchInProgress = true
147
- searchFiles ( query: searchText)
148
+ await searchFiles ( query: searchText)
149
+ searchTask = nil
148
150
}
149
151
}
150
152
@@ -154,20 +156,30 @@ struct FileListView: View {
154
156
// TODO: Instead of disabling cancel button, change the logic to cancel search API call
155
157
// TODO: Searches can be very expensive, so having a cancel option would be nice
156
158
Button ( action: {
157
- guard !searchInProgress else { return }
158
- Log . debug ( " 🔙 Search cancelled " )
159
+ if let task = searchTask {
160
+ Log . debug ( " 🔙 Search cancelled mid request " )
161
+ task. cancel ( )
162
+ statusMessage = StatusPayload (
163
+ text: " ✖️ Search cancelled " ,
164
+ color: . yellow,
165
+ duration: 3.5
166
+ )
167
+ searchTask = nil
168
+ } else {
169
+ Log . debug ( " 🔙 Search cancelled - no task in flight " )
170
+ }
171
+ // TODO: Reset searchType and add error handling back
159
172
searchClicked = false
160
173
searchInProgress = false
161
174
viewModel. searchResults. removeAll ( )
162
175
pathStack = previousPathStack
163
176
viewModel. fetchFiles ( at: pathStack. last ?? " / " )
164
177
} ) {
165
178
Image ( systemName: " xmark.circle.fill " )
166
- . foregroundColor ( searchInProgress ? . gray : . red)
179
+ . foregroundColor ( . red)
167
180
. font ( . system( size: 24 ) )
168
181
}
169
182
. padding ( . trailing, 0 )
170
- . disabled ( searchInProgress)
171
183
}
172
184
. padding ( . horizontal)
173
185
@@ -770,25 +782,28 @@ struct FileListView: View {
770
782
return URL ( string: " \( serverURL) /api/search/ \( removePrefix ( urlPath: searchLocation) ) ?query= \( encodedQuery) " )
771
783
}
772
784
773
- func searchFiles( query: String ) {
785
+ func searchFiles( query: String ) async {
774
786
Log . debug ( " 🔍 Searching for: \( query) " )
775
787
guard let serverURL = auth. serverURL, let token = auth. token else {
776
- DispatchQueue . main . async {
788
+ await MainActor . run {
777
789
errorMessage = " Invalid authorization. Please log out and log back in. "
778
790
searchInProgress = false
779
791
}
780
792
return
781
793
}
782
794
783
795
guard let url = getSearchURL ( serverURL: serverURL, query: query) else {
784
- errorMessage = " Invalid search: \( query) "
785
- searchInProgress = false
796
+ await MainActor . run {
797
+ errorMessage = " Invalid search: \( query) "
798
+ searchInProgress = false
799
+ }
786
800
Log . error ( " ❌ Failed to generate search URL " )
787
801
return
788
802
}
803
+
789
804
Log . debug ( " 🔍 Search URL: \( url. relativePath) " )
790
805
791
- DispatchQueue . main . async {
806
+ await MainActor . run {
792
807
viewModel. isLoading = true
793
808
errorMessage = nil
794
809
}
@@ -797,46 +812,36 @@ struct FileListView: View {
797
812
request. httpMethod = " GET "
798
813
request. setValue ( token, forHTTPHeaderField: " X-Auth " )
799
814
800
- URLSession . shared. dataTask ( with: request) { data, _, error in
801
- DispatchQueue . main. async {
815
+ do {
816
+ let ( data, _) = try await URLSession . shared. data ( for: request)
817
+
818
+ guard !Task. isCancelled else { return }
819
+
820
+ let results = try JSONDecoder ( ) . decode ( [ FileItemSearch ] . self, from: data)
821
+
822
+ await MainActor . run {
802
823
viewModel. isLoading = false
803
- searchInProgress = false // ✅ re-enable cancel when done
804
- }
824
+ searchInProgress = false
805
825
806
- if let error = error {
807
- DispatchQueue . main. async {
808
- errorMessage = error. localizedDescription
809
- }
810
- return
811
- }
826
+ let typeHead = searchType. map { String ( describing: $0) } ?? " files "
812
827
813
- guard let data = data else {
814
- DispatchQueue . main. async {
815
- errorMessage = " No data received "
828
+ if results. isEmpty {
829
+ viewModel. errorMessage = " No \( typeHead) found for: \( query) "
830
+ } else {
831
+ viewModel. searchResults = results
832
+ let typeDescription = typeHead == " files " ? typeHead : " \( typeHead) files "
833
+ Log . info ( " 🔍 Search returned \( results. count) \( typeDescription) " )
816
834
}
817
- return
818
835
}
819
-
820
- do {
821
- let results = try JSONDecoder ( ) . decode ( [ FileItemSearch ] . self, from: data)
822
- DispatchQueue . main. async {
823
- // TODO: Too much logic for something simple - just add a new case "files" in the parent enum
824
- let typeHead : String = searchType != nil ? String ( describing: searchType!) : " files "
825
- if results. isEmpty {
826
- viewModel. errorMessage = " No \( typeHead) found for: \( query) "
827
- } else {
828
- viewModel. searchResults = results
829
- let typeDescription = typeHead == " files " ? typeHead : " \( typeHead) files "
830
- Log . info ( " 🔍 Search returned \( results. count) \( typeDescription) " )
831
- }
832
- }
833
- } catch {
834
- DispatchQueue . main. async {
835
- errorMessage = " Failed to parse search results "
836
- }
837
- Log . error ( " Search decode error: \( error. localizedDescription) " )
836
+ } catch {
837
+ guard !Task. isCancelled else { return }
838
+ await MainActor . run {
839
+ viewModel. isLoading = false
840
+ searchInProgress = false
841
+ errorMessage = " Failed to perform search: \( error. localizedDescription) "
838
842
}
839
- } . resume ( )
843
+ Log . error ( " Search error: \( error. localizedDescription) " )
844
+ }
840
845
}
841
846
842
847
func deleteSession( ) {
0 commit comments