From 16f0c78449942e6401224eda0e5910faa8be6f97 Mon Sep 17 00:00:00 2001 From: Mohammad Ausaf <75237046+AusafMo@users.noreply.github.com> Date: Sun, 28 Jun 2026 13:05:38 +0530 Subject: [PATCH] fix: clear search state on tab switch --- searchy/ContentView.swift | 28 ++++++++++++++++++--- searchy/Managers.swift | 50 +++++++++++++++++++++++++++++++++++--- searchy/SettingsView.swift | 2 ++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/searchy/ContentView.swift b/searchy/ContentView.swift index 8ff564e..d4bb63b 100644 --- a/searchy/ContentView.swift +++ b/searchy/ContentView.swift @@ -232,6 +232,26 @@ struct ContentView: View { } } + private func selectTab(_ tab: AppTab) { + guard activeTab != tab else { return } + clearTransientSearchState() + activeTab = tab + } + + private func clearTransientSearchState() { + searchDebounceTimer?.invalidate() + searchDebounceTimer = nil + previewTimer?.invalidate() + previewTimer = nil + searchText = "" + pastedImage = nil + previewResult = nil + showPreviewPanel = false + isSearchFocused = false + isGlobalSearchFocused = false + searchManager.clearResults() + } + // MARK: - Atelier Sidebar private var atelierSidebar: some View { let sidebarItems: [(id: AppTab, label: String, icon: String, count: Int?, badge: Bool)] = [ @@ -273,7 +293,7 @@ struct ContentView: View { ForEach(sidebarItems, id: \.id) { item in Button(action: { withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - activeTab = item.id + selectTab(item.id) } }) { HStack(spacing: 10) { @@ -320,7 +340,7 @@ struct ContentView: View { ForEach(recentSearchQueries, id: \.self) { query in Button(action: { - activeTab = .search + selectTab(.search) searchText = query performSearch() }) { @@ -5928,7 +5948,7 @@ struct ContentView: View { .font(.system(size: 12)) .foregroundColor(p.ink3) .multilineTextAlignment(.center) - Button(action: { activeTab = .setup }) { + Button(action: { selectTab(.setup) }) { HStack(spacing: 6) { Image(systemName: "folder.badge.plus") .font(.system(size: 12)) @@ -6224,7 +6244,7 @@ struct ContentView: View { let digit = chars.first?.wholeNumberValue, digit >= 1 && digit <= 6 { let tabs: [AppTab] = [.faces, .search, .volumes, .duplicates, .gallery, .setup] DispatchQueue.main.async { - self.activeTab = tabs[digit - 1] + self.selectTab(tabs[digit - 1]) } return nil } diff --git a/searchy/Managers.swift b/searchy/Managers.swift index c4d07a0..fc4ee03 100644 --- a/searchy/Managers.swift +++ b/searchy/Managers.swift @@ -9,6 +9,9 @@ class SearchManager: ObservableObject { @Published private(set) var errorMessage: String? = nil @Published private(set) var searchStats: SearchStats? = nil + private var searchTask: Task? + private var searchGeneration = 0 + private init() {} private var serverURL: URL? { @@ -20,6 +23,9 @@ class SearchManager: ObservableObject { func search(query: String, numberOfResults: Int = 5, ocrWeight: Double = 0.5) { guard !isSearching else { return } + searchTask?.cancel() + searchGeneration += 1 + let generation = searchGeneration DispatchQueue.main.async { self.isSearching = true @@ -27,10 +33,13 @@ class SearchManager: ObservableObject { // Don't clear results — keep old results visible while searching } - Task { + searchTask = Task { do { let response = try await self.performSearch(query: query, numberOfResults: numberOfResults, ocrWeight: ocrWeight) + guard !Task.isCancelled else { return } + DispatchQueue.main.async { + guard self.searchGeneration == generation else { return } let filteredResults = response.results.filter { $0.similarity >= SearchPreferences.shared.similarityThreshold } @@ -39,7 +48,10 @@ class SearchManager: ObservableObject { self.isSearching = false } } catch { + guard !Task.isCancelled else { return } + DispatchQueue.main.async { + guard self.searchGeneration == generation else { return } self.results = [] self.errorMessage = error.localizedDescription self.isSearching = false @@ -77,18 +89,39 @@ class SearchManager: ObservableObject { return response } func cancelSearch() { - DispatchQueue.main.async { + searchTask?.cancel() + searchTask = nil + searchGeneration += 1 + + let reset = { self.isSearching = false self.errorMessage = nil } + + if Thread.isMainThread { + reset() + } else { + DispatchQueue.main.async(execute: reset) + } } func clearResults() { - DispatchQueue.main.async { + searchTask?.cancel() + searchTask = nil + searchGeneration += 1 + + let reset = { self.results = [] + self.isSearching = false self.searchStats = nil self.errorMessage = nil } + + if Thread.isMainThread { + reset() + } else { + DispatchQueue.main.async(execute: reset) + } } func loadRecentImages(completion: @escaping ([SearchResult]) -> Void) { @@ -133,6 +166,9 @@ class SearchManager: ObservableObject { func findSimilar(imagePath: String, numberOfResults: Int = 20) { guard !isSearching else { return } + searchTask?.cancel() + searchGeneration += 1 + let generation = searchGeneration DispatchQueue.main.async { self.isSearching = true @@ -141,16 +177,22 @@ class SearchManager: ObservableObject { self.searchStats = nil } - Task { + searchTask = Task { do { let response = try await self.performFindSimilar(imagePath: imagePath, numberOfResults: numberOfResults) + guard !Task.isCancelled else { return } + DispatchQueue.main.async { + guard self.searchGeneration == generation else { return } self.results = response.results self.searchStats = response.stats self.isSearching = false } } catch { + guard !Task.isCancelled else { return } + DispatchQueue.main.async { + guard self.searchGeneration == generation else { return } self.errorMessage = error.localizedDescription self.isSearching = false } diff --git a/searchy/SettingsView.swift b/searchy/SettingsView.swift index a8f779d..5bbe695 100644 --- a/searchy/SettingsView.swift +++ b/searchy/SettingsView.swift @@ -1519,9 +1519,11 @@ struct SettingsView: View { } } +#if canImport(PreviewsMacros) #Preview { SettingsView() } +#endif // Extension to support Python file types extension UTType { static var pythonScript: UTType {