From ffc50a6b927afd16601e0bc01a53d76ae6069ba3 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 03 Jun 2026 21:22:41 +0800
Subject: [PATCH] Refresh app index for quick search

---
 Apptag/ContentView.swift          |   17 +++++++-
 Apptag/DataLayer.swift            |   40 ++++++++++++++++++--
 Apptag/AppLibraryController.swift |    4 +-
 3 files changed, 52 insertions(+), 9 deletions(-)

diff --git a/Apptag/AppLibraryController.swift b/Apptag/AppLibraryController.swift
index 090a31b..aa2d4db 100644
--- a/Apptag/AppLibraryController.swift
+++ b/Apptag/AppLibraryController.swift
@@ -23,8 +23,8 @@
 }
 
 enum AppLibraryController {
-    static func refresh() -> AppLibraryRefreshResult {
-        let scannedApps = AppIndexer.scan()
+    static func refresh(useCache: Bool = true) -> AppLibraryRefreshResult {
+        let scannedApps = AppIndexer.scan(useCache: useCache)
         let reconciledStore = TagEditor.reconcileScannedApps(scannedApps)
         let smartStartResult = SmartStartService.runIfNeeded(
             apps: scannedApps,
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index f29d986..0fbfc7f 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -731,6 +731,7 @@
         quickSearchErrorMessage = nil
         quickSearchFocusToken &+= 1
         refreshQuickSearchResults()
+        refreshApps()
         NotificationCenter.default.post(
             name: .tagLauncherQuickSearchVisibilityChanged,
             object: nil,
@@ -777,7 +778,10 @@
 
     private func refreshQuickSearchResults() {
         let previousSelection = quickSearchSelectedID
-        quickSearchResults = QuickSearchEngine.search(quickSearchQuery, documents: quickSearchDocuments)
+        quickSearchResults = QuickSearchEngine.search(
+            quickSearchQuery,
+            documents: quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) }
+        )
 
         if quickSearchResults.isEmpty {
             quickSearchSelectedID = nil
@@ -2369,7 +2373,7 @@
         }
     }
 
-    func refreshApps(forceLayoutRefresh: Bool = false) {
+    func refreshApps(forceLayoutRefresh: Bool = false, useCache: Bool = true) {
         guard !refreshInProgress else {
             if forceLayoutRefresh {
                 refreshAgainAfterCurrent = true
@@ -2380,7 +2384,7 @@
 
         refreshInProgress = true
         DispatchQueue.global(qos: .userInitiated).async {
-            let result = AppLibraryController.refresh()
+            let result = AppLibraryController.refresh(useCache: useCache)
             DispatchQueue.main.async {
                 applyAppLibrarySnapshot(result.snapshot)
                 handleSmartStartRunResult(result.smartStartResult)
@@ -2417,6 +2421,13 @@
     ) {
         appGridInteraction.appDragModeActive = false
         endTagNavReorder()
+        guard FileManager.default.fileExists(atPath: app.path.path) else {
+            AppIndexer.invalidateScanCache()
+            refreshApps()
+            onFailure?()
+            return
+        }
+
         let closeQuickSearchAndOverlayBeforeOpening = closeQuickSearchOnSuccess
             && closeOverlayOnSuccess
             && (quickSearchCloseHidesOverlay || quickSearchOnlySession)
diff --git a/Apptag/DataLayer.swift b/Apptag/DataLayer.swift
index 9f9b9f0..91fed17 100644
--- a/Apptag/DataLayer.swift
+++ b/Apptag/DataLayer.swift
@@ -94,9 +94,16 @@
 // MARK: - App Scanner
 
 enum AppIndexer {
+    private struct SearchPathSignature: Equatable {
+        let path: String
+        let exists: Bool
+        let contentModificationTime: TimeInterval?
+    }
+
     private static let scanCacheLock = NSLock()
     private static var cachedScanApps: [AppInfo]? = nil
     private static var cachedScanAt: Date? = nil
+    private static var cachedSearchPathSignature: [SearchPathSignature]? = nil
     private static let scanCacheTTL: TimeInterval = 1800
 
     static let searchPaths: [URL] = [
@@ -120,12 +127,13 @@
 
     /// Scan all standard locations. Tags are annotated from TagDatabase by the caller.
     static func scan(useCache: Bool = true) -> [AppInfo] {
-        if useCache, let cached = cachedScanIfFresh() {
+        let searchPathSignature = currentSearchPathSignature()
+        if useCache, let cached = cachedScanIfFresh(matching: searchPathSignature) {
             return cached
         }
 
         let apps = performScan()
-        updateScanCache(apps)
+        updateScanCache(apps, searchPathSignature: searchPathSignature)
         return apps
     }
 
@@ -133,27 +141,51 @@
         scanCacheLock.lock()
         cachedScanApps = nil
         cachedScanAt = nil
+        cachedSearchPathSignature = nil
         scanCacheLock.unlock()
     }
 
-    private static func cachedScanIfFresh() -> [AppInfo]? {
+    private static func cachedScanIfFresh(matching searchPathSignature: [SearchPathSignature]) -> [AppInfo]? {
         scanCacheLock.lock()
         defer { scanCacheLock.unlock() }
 
         guard let cachedScanApps,
               let cachedScanAt,
+              cachedSearchPathSignature == searchPathSignature,
               Date().timeIntervalSince(cachedScanAt) < scanCacheTTL
         else { return nil }
         return cachedScanApps
     }
 
-    private static func updateScanCache(_ apps: [AppInfo]) {
+    private static func updateScanCache(_ apps: [AppInfo], searchPathSignature: [SearchPathSignature]) {
         scanCacheLock.lock()
         cachedScanApps = apps
         cachedScanAt = Date()
+        cachedSearchPathSignature = searchPathSignature
         scanCacheLock.unlock()
     }
 
+    private static func currentSearchPathSignature() -> [SearchPathSignature] {
+        searchPaths.map { url in
+            var isDirectory: ObjCBool = false
+            let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
+            guard exists, isDirectory.boolValue else {
+                return SearchPathSignature(
+                    path: url.path,
+                    exists: false,
+                    contentModificationTime: nil
+                )
+            }
+
+            let values = try? url.resourceValues(forKeys: [.contentModificationDateKey])
+            return SearchPathSignature(
+                path: url.path,
+                exists: true,
+                contentModificationTime: values?.contentModificationDate?.timeIntervalSinceReferenceDate
+            )
+        }
+    }
+
     private static func performScan() -> [AppInfo] {
         var seenResolvedPaths = Set<String>()
         var apps: [AppInfo] = []

--
Gitblit v1.9.3