From c5bdc7f1eb3ff4287beeeebb17d4f02f376a8d38 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 28 Aug 2026 17:34:19 +0800
Subject: [PATCH] feat: add quick search tag filtering
---
C1.source/Apptag/ContentView.swift | 71 ++++++++++++++++++++++++++++++++++-
1 files changed, 69 insertions(+), 2 deletions(-)
diff --git a/C1.source/Apptag/ContentView.swift b/C1.source/Apptag/ContentView.swift
index b1ca582..d9ae06f 100644
--- a/C1.source/Apptag/ContentView.swift
+++ b/C1.source/Apptag/ContentView.swift
@@ -447,6 +447,7 @@
@State private var quickSearchQuery = ""
@State private var quickSearchDocuments: [QuickSearchDocument] = []
@State private var quickSearchResults: [QuickSearchResult] = []
+ @State private var quickSearchSelectedTagName: String? = nil
@State private var quickSearchSelectedID: URL? = nil
@State private var quickSearchManualSelection = false
@State private var quickSearchFocusToken = 0
@@ -620,6 +621,9 @@
}
.onChange(of: defaultGroupName) { _, _ in
rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
+ if quickSearchVisible {
+ refreshQuickSearchResults()
+ }
}
.onChange(of: quickSearchQuery) { _, _ in
quickSearchErrorMessage = nil
@@ -816,6 +820,8 @@
QuickSearchPanelPresentationView(
query: $quickSearchQuery,
results: quickSearchResults,
+ tagFilters: quickSearchTagFilters,
+ selectedTagName: quickSearchSelectedTagName,
selectedID: quickSearchSelectedID,
focusToken: quickSearchFocusToken,
selectionScrollToken: quickSearchSelectionScrollToken,
@@ -826,7 +832,8 @@
errorMessage: quickSearchErrorMessage,
onCommand: handleQuickSearchCommand,
onHover: selectQuickSearchResult,
- onLaunch: launchQuickSearchResult
+ onLaunch: launchQuickSearchResult,
+ onTagFilterChange: selectQuickSearchTagFilter
)
.frame(width: 0, height: 0)
.allowsHitTesting(false)
@@ -843,6 +850,7 @@
&& !quickSearchResults.isEmpty
let visibleRows = min(max(1, quickSearchResults.count), quickSearchMaxVisibleRows(in: size))
return QuickSearchPanelMetrics.contentHeight(
+ hasTagFilters: !quickSearchTagFilters.isEmpty,
hasResultList: hasResultList,
visibleRows: visibleRows
)
@@ -855,6 +863,7 @@
private func quickSearchMaxVisibleRows(in size: CGSize) -> Int {
let bottomClearance: CGFloat = 84
let chromeHeight = QuickSearchPanelMetrics.headerHeight
+ + (quickSearchTagFilters.isEmpty ? 0 : QuickSearchPanelMetrics.tagFilterBarHeight)
+ QuickSearchPanelMetrics.dividerHeight
+ QuickSearchPanelMetrics.resultListVerticalInset * 2
let rowHeightWithSpacing = QuickSearchPanelMetrics.rowHeight
@@ -874,6 +883,7 @@
quickSearchVisible = true
quickSearchOpenedAt = Date()
quickSearchQuery = ""
+ quickSearchSelectedTagName = nil
quickSearchCompositionActive = false
quickSearchManualSelection = false
quickSearchErrorMessage = nil
@@ -928,6 +938,7 @@
quickSearchCompositionActive = false
quickSearchQuery = ""
quickSearchResults = []
+ quickSearchSelectedTagName = nil
quickSearchSelectedID = nil
quickSearchManualSelection = false
quickSearchErrorMessage = nil
@@ -977,9 +988,12 @@
private func refreshQuickSearchResults() {
let previousSelection = quickSearchSelectedID
+ let documents = quickSearchVisibleDocuments
+ let selectedTagName = validateQuickSearchSelectedTagName(against: quickSearchTagFilters)
quickSearchResults = QuickSearchEngine.search(
quickSearchQuery,
- documents: quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) }
+ documents: documents,
+ selectedTagName: selectedTagName
)
if quickSearchResults.isEmpty {
@@ -996,6 +1010,58 @@
quickSearchSelectedID = quickSearchResults.first?.id
quickSearchManualSelection = false
}
+ }
+
+ private var quickSearchVisibleDocuments: [QuickSearchDocument] {
+ quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) }
+ }
+
+ private var quickSearchTagFilters: [QuickSearchTagFilterOption] {
+ let availableTagNames = Set(
+ quickSearchVisibleDocuments
+ .flatMap(\.tagNames)
+ .filter(isQuickSearchFilterableTag)
+ )
+ guard !availableTagNames.isEmpty else { return [] }
+
+ let ordered = draggedTagNames
+ .filter { availableTagNames.contains($0) && isQuickSearchFilterableTag($0) }
+ let orderedSet = Set(ordered)
+ let remaining = availableTagNames
+ .filter { !orderedSet.contains($0) }
+ .sorted { $0.localizedStandardCompare($1) == .orderedAscending }
+
+ return (ordered + remaining).map { tagName in
+ let definition = tagDefinitions[tagName]
+ return QuickSearchTagFilterOption(
+ name: tagName,
+ colorIndex: definition?.color ?? tagColors[tagName] ?? 0,
+ customColor: definition?.customColor ?? tagCustomColors[tagName]
+ )
+ }
+ }
+
+ private func isQuickSearchFilterableTag(_ tagName: String) -> Bool {
+ let name = tagName.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !name.isEmpty, tagDefinitions[name] != nil else { return false }
+ return !AppContainerID.isReservedGroupName(name, defaultGroupName: defaultGroupName)
+ }
+
+ private func validateQuickSearchSelectedTagName(against filters: [QuickSearchTagFilterOption]) -> String? {
+ guard let quickSearchSelectedTagName else { return nil }
+ guard filters.contains(where: { $0.name == quickSearchSelectedTagName }) else {
+ self.quickSearchSelectedTagName = nil
+ return nil
+ }
+ return quickSearchSelectedTagName
+ }
+
+ private func selectQuickSearchTagFilter(_ tagName: String?) {
+ quickSearchSelectedTagName = tagName
+ quickSearchErrorMessage = nil
+ quickSearchManualSelection = false
+ quickSearchFocusToken &+= 1
+ refreshQuickSearchResults()
}
private func handleQuickSearchCommand(_ command: QuickSearchCommand) {
@@ -2429,6 +2495,7 @@
quickSearchFocusToken &+= 1
}
quickSearchOpenedAt = Date()
+ quickSearchSelectedTagName = nil
refreshQuickSearchResults()
refreshAppsForQuickSearch()
NotificationCenter.default.post(
--
Gitblit v1.9.3