From 8fec0d26e8637a1c96dbcac841417fdd176d480a Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 07 Jun 2026 01:28:37 +0800
Subject: [PATCH] docs: add App Store Connect filling guide

---
 Apptag/ContentView.swift |   93 +++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 79 insertions(+), 14 deletions(-)

diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 0fbfc7f..b9cd83f 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -376,6 +376,7 @@
     @State private var refreshInProgress = false
     @State private var refreshAgainAfterCurrent = false
     @State private var refreshAgainForceLayout = false
+    @State private var refreshAgainUseCache = true
     @FocusState private var bubbleNoteFocused: Bool
 
     // Quick Search
@@ -389,6 +390,7 @@
     @State private var quickSearchSelectionScrollToken = 0
     @State private var quickSearchCloseHidesOverlay = false
     @State private var quickSearchOnlySession = false
+    @State private var quickSearchOpenedAt: Date? = nil
     @State private var quickSearchErrorMessage: String? = nil
     @State private var initialQuickSearchConsumed = false
 
@@ -531,7 +533,7 @@
         }
         .onAppear {
             refreshNotchHeight()
-            refreshAppsIfNeeded()
+            refreshAppsForOverlay()
             if let initialQuickSearchSource, !initialQuickSearchConsumed {
                 initialQuickSearchConsumed = true
                 if initialQuickSearchSource == QuickSearchOpenSource.globalHidden {
@@ -542,7 +544,9 @@
                     quickSearchVisible = true
                     quickSearchFocusToken &+= 1
                 }
+                quickSearchOpenedAt = Date()
                 refreshQuickSearchResults()
+                refreshAppsForQuickSearch()
                 NotificationCenter.default.post(
                     name: .tagLauncherQuickSearchVisibilityChanged,
                     object: nil,
@@ -556,7 +560,7 @@
         .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in
             resetTransientDragState()
             refreshNotchHeight()
-            refreshAppsIfNeeded()
+            refreshAppsForOverlay()
         }
         .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in
             resetTransientDragState()
@@ -568,8 +572,9 @@
             let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay
             openQuickSearch(source: source)
         }
-        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested)) { _ in
-            closeQuickSearch()
+        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested)) { notification in
+            let source = notification.userInfo?["source"] as? String
+            closeQuickSearch(dismissalSource: source)
         }
         .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
             guard allApps.isEmpty, !refreshInProgress else { return }
@@ -663,7 +668,7 @@
                     .ignoresSafeArea()
                     .contentShape(Rectangle())
                     .onTapGesture {
-                        closeQuickSearch()
+                        closeQuickSearch(dismissalSource: QuickSearchDismissSource.backdrop)
                     }
                     .zIndex(899)
 
@@ -673,7 +678,7 @@
                     selectedID: quickSearchSelectedID,
                     focusToken: quickSearchFocusToken,
                     selectionScrollToken: quickSearchSelectionScrollToken,
-                    isLoading: quickSearchDocuments.isEmpty && refreshInProgress,
+                    isLoading: quickSearchShouldShowLoading,
                     maxVisibleRows: quickSearchMaxVisibleRows(in: proxy.size),
                     panelTopY: quickSearchPanelTopY(in: proxy.size),
                     panelHeight: quickSearchPanelContentHeight(in: proxy.size),
@@ -692,7 +697,7 @@
     }
 
     private func quickSearchPanelContentHeight(in size: CGSize) -> CGFloat {
-        let hasResultList = !(quickSearchDocuments.isEmpty && refreshInProgress)
+        let hasResultList = !quickSearchShouldShowLoading
             && quickSearchErrorMessage == nil
             && !quickSearchResults.isEmpty
         let visibleRows = min(max(1, quickSearchResults.count), quickSearchMaxVisibleRows(in: size))
@@ -726,12 +731,13 @@
         quickSearchCloseHidesOverlay = quickSearchCloseHidesOverlay
             || source == QuickSearchOpenSource.globalHidden
         quickSearchVisible = true
+        quickSearchOpenedAt = Date()
         quickSearchQuery = ""
         quickSearchManualSelection = false
         quickSearchErrorMessage = nil
         quickSearchFocusToken &+= 1
         refreshQuickSearchResults()
-        refreshApps()
+        refreshAppsForQuickSearch()
         NotificationCenter.default.post(
             name: .tagLauncherQuickSearchVisibilityChanged,
             object: nil,
@@ -750,10 +756,30 @@
             && !quickSearchVisible
     }
 
-    private func closeQuickSearch(notify: Bool = true, hideOverlayIfNeeded: Bool = true) {
+    private var quickSearchShouldShowLoading: Bool {
+        quickSearchVisible && refreshInProgress && quickSearchResults.isEmpty
+    }
+
+    private func closeQuickSearch(
+        notify: Bool = true,
+        hideOverlayIfNeeded: Bool = true,
+        dismissalSource: String? = nil
+    ) {
+        Diagnostics.log("content.quickSearch.closeRequest", [
+            "source": dismissalSource,
+            "notify": notify,
+            "hideOverlayIfNeeded": hideOverlayIfNeeded,
+            "quickSearchVisible": quickSearchVisible,
+            "quickSearchCloseHidesOverlay": quickSearchCloseHidesOverlay,
+            "quickSearchOnlySession": quickSearchOnlySession
+        ])
+        if shouldIgnoreEarlyQuickSearchDismiss(from: dismissalSource) {
+            return
+        }
         let shouldHideOverlay = quickSearchVisible && quickSearchCloseHidesOverlay && hideOverlayIfNeeded
         guard quickSearchVisible else { return }
         quickSearchVisible = false
+        quickSearchOpenedAt = nil
         quickSearchQuery = ""
         quickSearchResults = []
         quickSearchSelectedID = nil
@@ -774,6 +800,23 @@
         } else {
             quickSearchCloseHidesOverlay = false
         }
+    }
+
+    private func shouldIgnoreEarlyQuickSearchDismiss(from source: String?) -> Bool {
+        guard quickSearchVisible,
+              let quickSearchOpenedAt,
+              source == QuickSearchDismissSource.mouse || source == QuickSearchDismissSource.backdrop
+        else { return false }
+
+        let age = Date().timeIntervalSince(quickSearchOpenedAt)
+        let gracePeriod: TimeInterval = quickSearchOnlySession ? 1.25 : 0.30
+        guard age < gracePeriod else { return false }
+        Diagnostics.log("content.quickSearch.ignoredEarlyDismiss", [
+            "source": source,
+            "age": String(format: "%.3f", age),
+            "gracePeriod": String(format: "%.2f", gracePeriod)
+        ])
+        return true
     }
 
     private func refreshQuickSearchResults() {
@@ -800,6 +843,11 @@
     }
 
     private func handleQuickSearchCommand(_ command: QuickSearchCommand) {
+        Diagnostics.log("content.quickSearch.command", [
+            "command": String(describing: command),
+            "quickSearchCloseHidesOverlay": quickSearchCloseHidesOverlay,
+            "quickSearchOnlySession": quickSearchOnlySession
+        ])
         switch command {
         case .moveUp:
             moveQuickSearchSelection(by: -1)
@@ -1765,6 +1813,22 @@
         refreshApps()
     }
 
+    private func refreshAppsForOverlay() {
+        // Each overlay show creates a fresh ContentView with empty `allApps`. The indexer
+        // cache may still be warm from a prior overlay/settings scan, so path-signature
+        // alone must not skip the first hydrate or the grid spinner never clears.
+        guard allApps.isEmpty || AppIndexer.shouldRefreshForSearchPathChanges() else { return }
+        refreshApps()
+    }
+
+    private func refreshAppsForQuickSearch() {
+        guard allApps.isEmpty
+            || quickSearchDocuments.isEmpty
+            || AppIndexer.shouldRefreshForSearchPathChanges()
+        else { return }
+        refreshApps()
+    }
+
     private func refreshNotchHeight() {
         let mousePoint = NSEvent.mouseLocation
         let activeScreen = NSScreen.screens.first(where: {
@@ -2375,10 +2439,9 @@
 
     func refreshApps(forceLayoutRefresh: Bool = false, useCache: Bool = true) {
         guard !refreshInProgress else {
-            if forceLayoutRefresh {
-                refreshAgainAfterCurrent = true
-                refreshAgainForceLayout = true
-            }
+            refreshAgainAfterCurrent = true
+            refreshAgainForceLayout = refreshAgainForceLayout || forceLayoutRefresh
+            refreshAgainUseCache = refreshAgainUseCache && useCache
             return
         }
 
@@ -2394,9 +2457,11 @@
                 refreshInProgress = false
                 if refreshAgainAfterCurrent {
                     let shouldForceLayout = refreshAgainForceLayout
+                    let shouldUseCache = refreshAgainUseCache
                     refreshAgainAfterCurrent = false
                     refreshAgainForceLayout = false
-                    refreshApps(forceLayoutRefresh: shouldForceLayout)
+                    refreshAgainUseCache = true
+                    refreshApps(forceLayoutRefresh: shouldForceLayout, useCache: shouldUseCache)
                 }
             }
         }

--
Gitblit v1.9.3