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