From 18c90acda54cb46815a081ecfc36ea8f2b9e93fd Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 29 Jun 2026 00:12:04 +0800
Subject: [PATCH] Fix custom hotkey settings UI for 8.2.1

---
 src/Apptag/ContentView.swift |  875 ++++++++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 693 insertions(+), 182 deletions(-)

diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index a6011b8..743ec5f 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -8,9 +8,18 @@
     static let tagLauncherAppNoteEditingChanged = Notification.Name("TagLauncherAppNoteEditingChanged")
     static let tagLauncherDataDidChange = Notification.Name("TagLauncherDataDidChange")
     static let tagLauncherOpenPreferencesRequested = Notification.Name("TagLauncherOpenPreferencesRequested")
+    static let tagLauncherPreferencesTabRequested = Notification.Name("TagLauncherPreferencesTabRequested")
     static let tagLauncherOverlayDidShow = Notification.Name("TagLauncherOverlayDidShow")
     static let tagLauncherOverlayDidHide = Notification.Name("TagLauncherOverlayDidHide")
     static let tagLauncherModalInteractionChanged = Notification.Name("TagLauncherModalInteractionChanged")
+}
+
+enum SettingsTabTarget {
+    static let userInfoKey = "tab"
+    static let theme = "theme"
+    static let tags = "tags"
+    static let data = "data"
+    static let about = "about"
 }
 
 // MARK: - Edit Phase
@@ -82,6 +91,10 @@
     }
 
     func updateNSView(_ container: TextFieldContainer, context: Context) {
+        if let editor = container.textField.currentEditor() as? NSTextView,
+           editor.hasMarkedText() {
+            return
+        }
         if container.textField.stringValue != text {
             container.textField.stringValue = text
         }
@@ -106,6 +119,9 @@
         }
 
         func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
+            if textView.hasMarkedText() {
+                return false
+            }
             if commandSelector == #selector(NSResponder.insertNewline(_:)) {
                 onSubmit?()
                 return true
@@ -313,12 +329,17 @@
     var editingBubble: AppBubbleContext? = nil
     var pendingUncategorizedDrop: PendingUncategorizedDrop? = nil
     var pendingTagRemovalDrop: PendingTagRemovalDrop? = nil
+    var proPrompt: PendingProPrompt? = nil
     var uncategorizedDropSuppressFuturePrompt = false
     var tagRemovalDropSuppressFuturePrompt = false
+    var usageTipsCloseReminderVisible = false
+    var usageTipsCloseSuppressFuturePrompt = false
     var appDragResetToken = 0
     var bubbleDraftNote = ""
     var tagNavigationHoveredGroupName: String? = nil
     var tagNavigationAppDropTargetName: String? = nil
+    var tagNavigationLastHoverScrollID: String? = nil
+    var tagNavigationLastHoverScrollAt: Date? = nil
 }
 
 private struct EditActionFeedback: Identifiable {
@@ -340,17 +361,28 @@
     let tagName: String
 }
 
+private struct PendingProPrompt: Identifiable {
+    let id = UUID()
+    let feature: ProFeature
+    let noteQuotaStatus: ProNoteQuotaStatus?
+}
+
 struct ContentView: View {
     let hideOverlay: () -> Void
     private let initialQuickSearchSource: String?
 
     @Environment(\.colorScheme) private var colorScheme
+    @ObservedObject private var proEntitlement = ProEntitlementCenter.shared
     @State private var allApps: [AppInfo] = []
     @State private var displayGroups: [TagGroup] = []
     @State private var tagColors: [String: Int] = [:]
+    @State private var tagDefinitions: [String: TagDatabase.TagDef] = [:]
+    @State private var containerAppOrder: [String: [String]] = [:]
     @State private var groupLayoutVersion = 0
     @State private var appGridScrollTargetID: String? = nil
     @State private var appGridScrollRequestToken = 0
+    @State private var selectedUsageTipIndex = 0
+    @State private var usageTipsHovered = false
 
     // Edit mode
     @State private var editPhase: EditPhase = .none
@@ -372,6 +404,8 @@
     @State private var smartStartNotice: SmartStartNotice? = nil
     @State private var pendingSmartStartDraft: SmartCategorizationDraft? = nil
     @State private var refreshInProgress = false
+    @State private var loadingSpinnerVisible = false
+    @State private var loadingSpinnerToken = 0
     @State private var refreshAgainAfterCurrent = false
     @State private var refreshAgainForceLayout = false
     @State private var refreshAgainUseCache = true
@@ -412,28 +446,68 @@
     @AppStorage("displayMode") private var displayMode = AppDefaults.displayMode
     @AppStorage("hideAppNames") private var hideAppNames = AppDefaults.hideAppNames
     @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
+    @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips
+    @AppStorage(AppGridTheme.storageKey) private var appGridThemeID = AppDefaults.appGridThemeID
+    @AppStorage(AppGridTheme.deepBlueTuningStorageKey) private var deepBlueThemeTuning = AppGridTheme.deepBlue.defaultColorTuning
+    @AppStorage(AppGridTheme.colorfulTuningStorageKey) private var colorfulThemeTuning = AppGridTheme.colorful.defaultColorTuning
     @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation
     @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false
     @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false
+    @AppStorage("skipUsageTipsCloseReminder") private var skipUsageTipsCloseReminder = false
 
     private let editSidebarWidth: CGFloat = 188
     private let editSidebarHorizontalInset: CGFloat = 12
     private let floatingControlsTrailingInset: CGFloat = 20
     private let floatingControlsReservedWidth: CGFloat = 120
+    private let loadingSpinnerDelay: TimeInterval = 0.25
     private var appBubbleDisabled: Bool {
-        appGridInteraction.appDragModeActive
+        usageTipsHovered
+            || appGridInteraction.appDragModeActive
             || appGridInteraction.pendingUncategorizedDrop != nil
             || appGridInteraction.pendingTagRemovalDrop != nil
+            || appGridInteraction.usageTipsCloseReminderVisible
+            || appGridInteraction.proPrompt != nil
     }
     private var appGridHighlightedGroupName: String? {
         appGridInteraction.tagNavigationHoveredGroupName
             ?? (isColorlessContainerMode ? filledColorlessContainer : nil)
     }
     private let rightSidebarFloatingClearance: CGFloat = 44
+    private let tagNavigationHoverScrollDelay: TimeInterval = 0.14
+    private let tagNavigationHoverScrollInterval: TimeInterval = 0.22
     private var floatingButtonSurfaceColor: Color {
-        colorScheme == .dark
+        return colorScheme == .dark
             ? Color.white.opacity(0.10)
             : Color.white.opacity(0.78)
+    }
+
+    private var appGridTheme: AppGridTheme {
+        AppGridTheme(storedID: appGridThemeID)
+    }
+
+    private var renderedAppGridTheme: AppGridTheme {
+        editPhase == .none
+            ? ProEntitlementPolicy.effectiveTheme(for: appGridTheme)
+            : .defaultLight
+    }
+
+    private var renderedAppGridThemeTuning: Double? {
+        guard editPhase == .none else { return nil }
+        return ProEntitlementPolicy.effectiveThemeTuning(
+            for: appGridTheme,
+            storedTuning: storedThemeTuning(for: appGridTheme)
+        )
+    }
+
+    private func storedThemeTuning(for theme: AppGridTheme) -> Double? {
+        switch theme {
+        case .deepBlue:
+            return deepBlueThemeTuning
+        case .colorful:
+            return colorfulThemeTuning
+        case .defaultLight, .black, .pink, .purple, .green, .blue:
+            return nil
+        }
     }
 
     private var isSideLayout: Bool {
@@ -448,6 +522,23 @@
         displayMode == "container" || displayMode == "gridContainer"
     }
 
+    private var shouldShowUsageTips: Bool {
+        !hideUsageTips && !allApps.isEmpty && !quickSearchOnlySession
+    }
+
+    private var appGridUsageTips: [AppGridUsageTip] {
+        [
+            AppGridUsageTip(id: 1, titleKey: "usageTips.tip1.title", detailKey: "usageTips.tip1.detail"),
+            AppGridUsageTip(id: 2, titleKey: "usageTips.tip2.title", detailKey: "usageTips.tip2.detail"),
+            AppGridUsageTip(id: 3, titleKey: "usageTips.tip3.title", detailKey: "usageTips.tip3.detail"),
+            AppGridUsageTip(id: 4, titleKey: "usageTips.tip4.title", detailKey: "usageTips.tip4.detail"),
+            AppGridUsageTip(id: 5, titleKey: "usageTips.tip5.title", detailKey: "usageTips.tip5.detail"),
+            AppGridUsageTip(id: 6, titleKey: "usageTips.tip6.title", detailKey: "usageTips.tip6.detail"),
+            AppGridUsageTip(id: 7, titleKey: "usageTips.tip7.title", detailKey: "usageTips.tip7.detail"),
+            AppGridUsageTip(id: 8, titleKey: "usageTips.tip8.title", detailKey: "usageTips.tip8.detail"),
+        ]
+    }
+
     private var shouldRenderAppGridBehindQuickSearch: Bool {
         if quickSearchOnlySession {
             return false
@@ -456,175 +547,174 @@
     }
 
     var body: some View {
+        contentStack
+            .onAppear(perform: handleContentAppear)
+            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow), perform: handleOverlayDidShow)
+            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide), perform: handleOverlayDidHide)
+            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchRequested), perform: handleQuickSearchRequested)
+            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested), perform: handleQuickSearchDismissRequested)
+            .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification), perform: handleApplicationDidBecomeActive)
+            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherDataDidChange), perform: handleDataDidChange)
+            .onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange), perform: handleAppLanguageDidChange)
+            .onChange(of: editPhase) { _, newPhase in
+                let active = newPhase != .none
+                dismissAppBubble()
+                NotificationCenter.default.post(
+                    name: .tagLauncherEditModeChanged,
+                    object: nil,
+                    userInfo: ["active": active]
+                )
+            }
+            .onChange(of: bubbleNoteFocused) { _, focused in
+                if appGridInteraction.editingBubble != nil && !focused {
+                    commitBubbleNote()
+                }
+            }
+            .onChange(of: draggedTagNames) { _, newOrder in
+                rebuildDisplayGroups(apps: allApps, tagOrder: newOrder)
+            }
+            .onChange(of: defaultGroupName) { _, _ in
+                rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
+            }
+            .onChange(of: quickSearchQuery) { _, _ in
+                quickSearchErrorMessage = nil
+                refreshQuickSearchResults()
+            }
+            .onChange(of: appGridInteraction.pendingUncategorizedDrop != nil) { _, _ in
+                publishModalInteractionState()
+            }
+            .onChange(of: appGridInteraction.pendingTagRemovalDrop != nil) { _, _ in
+                publishModalInteractionState()
+            }
+            .onChange(of: appGridInteraction.proPrompt != nil) { _, _ in
+                publishModalInteractionState()
+            }
+            .onChange(of: smartStartNotice != nil) { _, _ in
+                publishModalInteractionState()
+            }
+            .onChange(of: proEntitlement.accessState) { _, newState in
+                handleProAccessStateChange(newState)
+            }
+            .onDisappear {
+                NotificationCenter.default.post(
+                    name: .tagLauncherModalInteractionChanged,
+                    object: nil,
+                    userInfo: ["active": false]
+                )
+            }
+    }
+
+    @ViewBuilder
+    private var contentStack: some View {
         ZStack {
             if shouldRenderAppGridBehindQuickSearch {
-                VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
-                    .ignoresSafeArea()
-                    .allowsHitTesting(false)
+                appGridBackground
             }
 
             if shouldRenderAppGridBehindQuickSearch {
-                if notchHeight > 0 {
-                    VStack {
-                        Rectangle().fill(.black)
-                            .frame(height: notchHeight)
-                            .ignoresSafeArea(edges: .top)
-                        Spacer()
-                    }
-                    .allowsHitTesting(false)
-                }
-
-                switch editPhase {
-                case .none:
-                    normalContent
-                case .editingTags:
-                    editTagsView
-                case .editingApps:
-                    editAppsView
-                }
-
-                uncommonAppBubbleOverlay
-                smartStartNoticeOverlay
-                editActionFeedbackOverlay
-                uncategorizedDropConfirmOverlay
-                tagRemovalDropConfirmOverlay
+                appGridInteractionLayer
             }
 
             quickSearchOverlay
 
-            if shouldRenderAppGridBehindQuickSearch, let message = appGridInteraction.dropWarningToast {
-                Text(message)
-                    .font(.system(size: 16, weight: .semibold))
-                    .foregroundStyle(.primary)
-                    .padding(.horizontal, 22)
-                    .padding(.vertical, 12)
-                    .background(
-                        RoundedRectangle(cornerRadius: 10)
-                            .fill(.ultraThickMaterial)
-                            .shadow(color: .black.opacity(0.22), radius: 18, y: 10)
-                    )
-                    .transition(.scale(scale: 0.96).combined(with: .opacity))
-                    .allowsHitTesting(false)
-            }
-
-            if shouldRenderAppGridBehindQuickSearch && appGridInteraction.dropRefreshVisible {
-                Color.black.opacity(0.08)
-                    .ignoresSafeArea()
-                    .transition(.opacity)
-                    .allowsHitTesting(false)
-
-                ProgressView()
-                    .progressViewStyle(.circular)
-                    .controlSize(.large)
-                    .scaleEffect(1.15)
-                    .padding(24)
-                    .background(
-                        RoundedRectangle(cornerRadius: 12)
-                            .fill(.ultraThickMaterial)
-                            .shadow(color: .black.opacity(0.26), radius: 24, y: 12)
-                    )
-                    .transition(.scale(scale: 0.92).combined(with: .opacity))
-                    .allowsHitTesting(false)
-            }
+            dropWarningToastOverlay
+            dropRefreshOverlay
         }
-        .onAppear {
-            refreshNotchHeight()
-            refreshAppsForOverlay()
-            if let initialQuickSearchSource, !initialQuickSearchConsumed {
-                initialQuickSearchConsumed = true
-                if initialQuickSearchSource == QuickSearchOpenSource.globalHidden {
-                    quickSearchCloseHidesOverlay = true
-                    quickSearchOnlySession = true
-                }
-                if !quickSearchVisible {
-                    quickSearchVisible = true
-                    quickSearchFocusToken &+= 1
-                }
-                quickSearchOpenedAt = Date()
-                refreshQuickSearchResults()
-                refreshAppsForQuickSearch()
-                NotificationCenter.default.post(
-                    name: .tagLauncherQuickSearchVisibilityChanged,
-                    object: nil,
-                    userInfo: [
-                        "active": true,
-                        "hideOverlayOnClose": quickSearchCloseHidesOverlay
-                    ]
+    }
+
+    @ViewBuilder
+    private var appGridInteractionLayer: some View {
+        if notchHeight > 0 {
+            VStack {
+                Rectangle().fill(.black)
+                    .frame(height: notchHeight)
+                    .ignoresSafeArea(edges: .top)
+                Spacer()
+            }
+            .allowsHitTesting(false)
+        }
+
+        switch editPhase {
+        case .none:
+            normalContent
+        case .editingTags:
+            editTagsView
+        case .editingApps:
+            editAppsView
+        }
+
+        uncommonAppBubbleOverlay
+        smartStartNoticeOverlay
+        editActionFeedbackOverlay
+        uncategorizedDropConfirmOverlay
+        tagRemovalDropConfirmOverlay
+        usageTipsCloseReminderOverlay
+        proAccessPromptOverlay
+    }
+
+    @ViewBuilder
+    private var dropWarningToastOverlay: some View {
+        if shouldRenderAppGridBehindQuickSearch, let message = appGridInteraction.dropWarningToast {
+            Text(message)
+                .font(.system(size: 16, weight: .semibold))
+                .foregroundStyle(.primary)
+                .padding(.horizontal, 22)
+                .padding(.vertical, 12)
+                .background(
+                    RoundedRectangle(cornerRadius: 10)
+                        .fill(.ultraThickMaterial)
+                        .shadow(color: .black.opacity(0.22), radius: 18, y: 10)
+                )
+                .transition(.scale(scale: 0.96).combined(with: .opacity))
+                .allowsHitTesting(false)
+        }
+    }
+
+    @ViewBuilder
+    private var proAccessPromptOverlay: some View {
+        if let prompt = appGridInteraction.proPrompt {
+            ZStack {
+                Color.black.opacity(0.16)
+                    .ignoresSafeArea()
+
+                ProUpgradePromptView(
+                    title: tr(prompt.feature.titleKey),
+                    message: proPromptMessage(for: prompt),
+                    benefitText: tr(prompt.feature.benefitKey),
+                    operationText: proEntitlement.operationState.statusMessageKey.map(tr),
+                    unlockTitle: tr("pro.card.unlock"),
+                    restoreTitle: tr("pro.card.restore"),
+                    isBusy: proEntitlement.operationState.isBusy,
+                    onClose: dismissProPrompt,
+                    onUnlock: { proEntitlement.purchasePro() },
+                    onRestore: { proEntitlement.restorePurchases() }
                 )
             }
+            .transition(.opacity)
+            .zIndex(920)
         }
-        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in
-            resetTransientDragState()
-            refreshNotchHeight()
-            refreshAppsForOverlay()
-        }
-        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in
-            resetTransientDragState()
-            closeQuickSearch(notify: true, hideOverlayIfNeeded: false)
-            quickSearchOnlySession = false
-            quickSearchCloseHidesOverlay = false
-        }
-        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchRequested)) { notification in
-            let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay
-            openQuickSearch(source: source)
-        }
-        .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 }
-            refreshApps()
-        }
-        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherDataDidChange)) { _ in
-            refreshApps(forceLayoutRefresh: true)
-        }
-        .onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
-            let appsSnapshot = allApps
-            DispatchQueue.global(qos: .userInitiated).async {
-                _ = AppleDefaultAppCatalog.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot)
-                _ = SmartStartService.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot)
-                DispatchQueue.main.async {
-                    refreshApps(forceLayoutRefresh: true)
-                }
-            }
-        }
-        .onChange(of: editPhase) { _, newPhase in
-            let active = newPhase != .none
-            dismissAppBubble()
-            NotificationCenter.default.post(
-                name: .tagLauncherEditModeChanged,
-                object: nil,
-                userInfo: ["active": active]
-            )
-        }
-        .onChange(of: bubbleNoteFocused) { _, focused in
-            if appGridInteraction.editingBubble != nil && !focused {
-                commitBubbleNote()
-            }
-        }
-        .onChange(of: draggedTagNames) { _, newOrder in
-            rebuildDisplayGroups(apps: allApps, tagOrder: newOrder)
-        }
-        .onChange(of: defaultGroupName) { _, _ in
-            rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
-        }
-        .onChange(of: quickSearchQuery) { _, _ in
-            quickSearchErrorMessage = nil
-            refreshQuickSearchResults()
-        }
-        .onChange(of: appGridInteraction.pendingUncategorizedDrop != nil) { _, _ in
-            publishModalInteractionState()
-        }
-        .onChange(of: appGridInteraction.pendingTagRemovalDrop != nil) { _, _ in
-            publishModalInteractionState()
-        }
-        .onDisappear {
-            NotificationCenter.default.post(
-                name: .tagLauncherModalInteractionChanged,
-                object: nil,
-                userInfo: ["active": false]
-            )
+    }
+
+    @ViewBuilder
+    private var dropRefreshOverlay: some View {
+        if shouldRenderAppGridBehindQuickSearch && appGridInteraction.dropRefreshVisible {
+            Color.black.opacity(0.08)
+                .ignoresSafeArea()
+                .transition(.opacity)
+                .allowsHitTesting(false)
+
+            ProgressView()
+                .progressViewStyle(.circular)
+                .controlSize(.large)
+                .scaleEffect(1.15)
+                .padding(24)
+                .background(
+                    RoundedRectangle(cornerRadius: 12)
+                        .fill(.ultraThickMaterial)
+                        .shadow(color: .black.opacity(0.26), radius: 24, y: 12)
+                )
+                .transition(.scale(scale: 0.92).combined(with: .opacity))
+                .allowsHitTesting(false)
         }
     }
 
@@ -632,7 +722,13 @@
         NotificationCenter.default.post(
             name: .tagLauncherModalInteractionChanged,
             object: nil,
-            userInfo: ["active": appGridInteraction.pendingUncategorizedDrop != nil || appGridInteraction.pendingTagRemovalDrop != nil]
+            userInfo: [
+                "active": smartStartNotice != nil
+                    || appGridInteraction.pendingUncategorizedDrop != nil
+                    || appGridInteraction.pendingTagRemovalDrop != nil
+                    || appGridInteraction.usageTipsCloseReminderVisible
+                    || appGridInteraction.proPrompt != nil
+            ]
         )
     }
 
@@ -915,6 +1011,40 @@
         }
     }
 
+    private var appGridBackground: some View {
+        let theme = renderedAppGridTheme
+        let tuning = renderedAppGridThemeTuning
+        return ZStack {
+            if theme.usesVisualEffectBackdrop {
+                VisualEffectView(
+                    material: theme.material,
+                    blendingMode: .behindWindow
+                )
+            }
+            if theme.isPureBlackBackground {
+                Color.black
+            }
+            if !theme.isDefaultLight {
+                LinearGradient(
+                    colors: theme.backgroundBaseColors(tuning: tuning),
+                    startPoint: .topLeading,
+                    endPoint: .bottomTrailing
+                )
+                LinearGradient(
+                    colors: theme.backgroundAccentColors(tuning: tuning),
+                    startPoint: .topTrailing,
+                    endPoint: .bottomLeading
+                )
+                if theme.backgroundDimmingOpacity > 0 {
+                    Rectangle()
+                        .fill(Color.black.opacity(theme.backgroundDimmingOpacity))
+                }
+            }
+        }
+        .ignoresSafeArea()
+        .allowsHitTesting(false)
+    }
+
     private var floatingActionButtons: some View {
         HStack(spacing: 8) {
             floatingOverlayButton(systemImage: "pencil.line") {
@@ -1012,6 +1142,9 @@
             onActivate: { tagID in
                 activateTagNavigation(tagID)
             },
+            onDoubleActivate: { _ in
+                openTagSettingsFromNavigation()
+            },
             onHoverChange: { tagID, active in
                 handleTagNavigationHover(tagID, active: active)
             },
@@ -1036,6 +1169,14 @@
             onAppDrop: { path, source, targetTag, copy in
                 dropAppOnTagNavigation(path: path, sourceTag: source, targetTag: targetTag, copy: copy)
             }
+        )
+    }
+
+    private func openTagSettingsFromNavigation() {
+        NotificationCenter.default.post(
+            name: .tagLauncherOpenPreferencesRequested,
+            object: nil,
+            userInfo: [SettingsTabTarget.userInfoKey: SettingsTabTarget.tags]
         )
     }
 
@@ -1100,7 +1241,9 @@
         Group {
             if allApps.isEmpty {
                 Spacer()
-                ProgressView().scaleEffect(0.8)
+                if loadingSpinnerVisible {
+                    ProgressView().scaleEffect(0.8)
+                }
                 Spacer()
             } else {
                 AppGridCollectionView(
@@ -1109,9 +1252,14 @@
                     displayMode: displayMode,
                     iconSize: iconSize,
                     showNames: !hideAppNames,
+                    appGridTheme: renderedAppGridTheme,
                     bubbleDisabled: appBubbleDisabled,
                     showUncommonAppBubbles: showUncommonAppBubbles,
                     highlightedGroupName: appGridHighlightedGroupName,
+                    bottomContentPadding: shouldShowUsageTips ? AppGridUsageTipsMetrics.reservedHeight : 0,
+                    usageTipsVisible: shouldShowUsageTips,
+                    usageTips: appGridUsageTips,
+                    selectedUsageTipIndex: $selectedUsageTipIndex,
                     contentRevision: groupLayoutVersion,
                     scrollTargetID: appGridScrollTargetID,
                     scrollRequestToken: appGridScrollRequestToken,
@@ -1124,13 +1272,18 @@
                     onDropOutsideGroup: { path, source, copy in
                         dropAppOutsideGroup(path: path, sourceTag: source, copy: copy)
                     },
+                    onReorderApps: { containerID, orderedPaths in
+                        reorderApps(inContainer: containerID, orderedPaths: orderedPaths)
+                    },
                     onGroupActivate: { groupName in
                         if isColorlessContainerMode {
                             toggleColorlessFill(groupName)
                         }
                     },
                     onScrollActivity: handleAppGridScrollActivity,
-                    onDragModeChange: { setAppDragMode($0) }
+                    onDragModeChange: { setAppDragMode($0) },
+                    onHideUsageTips: requestHideUsageTips,
+                    onUsageTipsHoverChange: handleUsageTipsHoverChange
                 )
             }
         }
@@ -1158,6 +1311,8 @@
                     isEditing: editing,
                     placement: placement,
                     arrowOffset: metrics.arrowOffset,
+                    noteQuotaText: editing ? noteQuotaHintText(for: context.app) : nil,
+                    noteQuotaWarning: editing && isNoteQuotaWarning(for: context.app),
                     draftNote: $appGridInteraction.bubbleDraftNote,
                     noteFocused: $bubbleNoteFocused,
                     onCommit: commitBubbleNote,
@@ -1192,17 +1347,32 @@
                 } label: {
                     Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right")
                         .font(.system(size: 12))
+                        .foregroundStyle(renderedAppGridTheme.editPrimaryTextColor)
+                        .padding(.horizontal, 8)
+                        .frame(height: 28)
+                        .background(
+                            RoundedRectangle(cornerRadius: 7, style: .continuous)
+                                .fill(renderedAppGridTheme.editControlSurfaceColor)
+                        )
+                        .overlay(
+                            RoundedRectangle(cornerRadius: 7, style: .continuous)
+                                .stroke(renderedAppGridTheme.editControlStrokeColor, lineWidth: 1)
+                        )
                 }
-                .buttonStyle(.bordered)
+                .buttonStyle(.plain)
+                .shadow(color: renderedAppGridTheme.editButtonShadowColor, radius: 10, y: 4)
                 Spacer()
-                Text(tr("edit.tags")).font(.headline)
+                Text(tr("edit.tags"))
+                    .font(.headline)
+                    .foregroundStyle(renderedAppGridTheme.editPrimaryTextColor)
                 Spacer()
             }
             .padding(.horizontal, 24)
             .padding(.top, notchHeight > 0 ? notchHeight + 10 : 20)
             .padding(.bottom, 12)
+            .background(renderedAppGridTheme.editToolbarSurfaceColor)
 
-            Divider().opacity(0.3)
+            Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1)
 
             TagEditorView(
                 tagColors: $tagColors,
@@ -1217,6 +1387,7 @@
     private var editAppsView: some View {
         VStack(spacing: 0) {
             EditAppsHeaderView(
+                theme: renderedAppGridTheme,
                 operation: editTagOperation,
                 hintText: editModeHintText,
                 confirmTitle: editConfirmTitle,
@@ -1230,11 +1401,12 @@
                 onConfirm: confirmAssign
             )
 
-            Divider().opacity(0.3)
+            Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1)
 
             HStack(spacing: 0) {
                 VStack(alignment: .leading, spacing: 4) {
                     EditAppsSidebarIntroView(
+                        theme: renderedAppGridTheme,
                         width: editSidebarWidth,
                         horizontalInset: editSidebarHorizontalInset
                     )
@@ -1266,10 +1438,14 @@
                     .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                 }
                 .frame(width: editSidebarWidth, alignment: .topLeading)
-                Rectangle().fill(.secondary.opacity(0.12)).frame(width: 1)
+                Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(width: 1)
 
                 if allApps.isEmpty {
-                    Spacer(); ProgressView().scaleEffect(0.8); Spacer()
+                    Spacer()
+                    if loadingSpinnerVisible {
+                        ProgressView().scaleEffect(0.8)
+                    }
+                    Spacer()
                 } else {
                     editAppsGrid
                 }
@@ -1293,12 +1469,12 @@
     private func editFlatGroup(_ group: TagGroup) -> some View {
         VStack(alignment: .leading, spacing: 0) {
             HStack(spacing: 0) {
-                Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1)
+                Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1)
                 Text(group.name)
                     .font(.system(size: tagFontSize, weight: .semibold))
-                    .foregroundStyle(.secondary)
+                    .foregroundStyle(renderedAppGridTheme.editSecondaryTextColor)
                     .padding(.horizontal, 10)
-                Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1)
+                Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1)
             }
             .padding(.bottom, 6)
 
@@ -1331,7 +1507,8 @@
             colorIndex: colorIndex,
             operation: editTagOperation,
             isSelected: isSelected,
-            isRemovableCandidate: isRemovableCandidate
+            isRemovableCandidate: isRemovableCandidate,
+            theme: renderedAppGridTheme
         ) {
             if isSelected {
                 selectedTagNames.remove(tagName)
@@ -1405,7 +1582,8 @@
         return EditableAppSelectionItem(
             app: app,
             iconSize: iconSize,
-            isSelected: isSelected
+            isSelected: isSelected,
+            theme: renderedAppGridTheme
         ) {
             toggleEditableAppSelection(app)
         }
@@ -1553,6 +1731,34 @@
         }
         .ignoresSafeArea()
         .allowsHitTesting(appGridInteraction.pendingTagRemovalDrop != nil)
+    }
+
+    private var usageTipsCloseReminderOverlay: some View {
+        GeometryReader { proxy in
+            if appGridInteraction.usageTipsCloseReminderVisible {
+                ZStack {
+                    Color.black.opacity(0.14)
+                        .ignoresSafeArea()
+
+                    UsageTipsCloseReminderBubble(
+                        title: tr("usageTips.closeReminder.title"),
+                        message: tr("usageTips.closeReminder.message"),
+                        previewCallout: tr("usageTips.closeReminder.previewCallout"),
+                        doNotRemindTitle: tr("usageTips.closeReminder.doNotRemind"),
+                        doNotRemind: $appGridInteraction.usageTipsCloseSuppressFuturePrompt,
+                        confirmTitle: tr("edit.confirm"),
+                        onCancel: dismissUsageTipsCloseReminder,
+                        onConfirm: confirmUsageTipsCloseReminder
+                    )
+                    .frame(width: min(760, max(420, proxy.size.width - 120)))
+                    .position(x: proxy.size.width / 2, y: proxy.size.height / 2)
+                    .transition(.scale(scale: 0.94).combined(with: .opacity))
+                }
+                .zIndex(712)
+            }
+        }
+        .ignoresSafeArea()
+        .allowsHitTesting(appGridInteraction.usageTipsCloseReminderVisible)
     }
 
     private func buildEditActionFeedback(for selectedApps: [AppInfo], tags: [String]) -> EditActionFeedback {
@@ -1771,13 +1977,27 @@
 
     private func makeDisplayGroups(apps: [AppInfo], tagOrder: [String]) -> [TagGroup] {
         let order = tagOrder.isEmpty ? TagEditor.orderedTagNames() : tagOrder
-        let rawGroups = AppIndexer.group(apps: apps, defaultGroupName: defaultGroupName, tagOrder: order)
+        let rawGroups = AppIndexer.group(
+            apps: apps,
+            defaultGroupName: defaultGroupName,
+            tagOrder: order,
+            tagDefinitions: tagDefinitions,
+            containerAppOrder: containerAppOrder
+        )
         return rawGroups.map { group in
             if group.name == defaultGroupName {
-                return TagGroup(name: tr("group.uncategorized"), apps: group.apps)
+                return TagGroup(
+                    containerID: group.containerID,
+                    name: tr("group.uncategorized"),
+                    apps: group.apps
+                )
             }
             if group.name == "Mac自带" {
-                return TagGroup(name: tr("group.appleBuiltIn"), apps: group.apps)
+                return TagGroup(
+                    containerID: group.containerID,
+                    name: tr("group.appleBuiltIn"),
+                    apps: group.apps
+                )
             }
             return group
         }
@@ -1814,16 +2034,24 @@
         // 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 }
+        let wasEmpty = allApps.isEmpty
+        hydrateFromLastAppLibrarySnapshotIfNeeded()
+        guard wasEmpty || AppIndexer.shouldRefreshForSearchPathChanges() else { return }
         refreshApps()
     }
 
     private func refreshAppsForQuickSearch() {
+        hydrateFromLastAppLibrarySnapshotIfNeeded()
         guard allApps.isEmpty
             || quickSearchDocuments.isEmpty
             || AppIndexer.shouldRefreshForSearchPathChanges()
         else { return }
         refreshApps()
+    }
+
+    private func hydrateFromLastAppLibrarySnapshotIfNeeded() {
+        guard allApps.isEmpty, let snapshot = AppLibraryController.lastSnapshot() else { return }
+        applyAppLibrarySnapshot(snapshot)
     }
 
     private func refreshNotchHeight() {
@@ -1836,6 +2064,13 @@
 
     private func handleAppGridScrollActivity() {
         if appGridInteraction.hoveredBubble != nil {
+            appGridInteraction.hoveredBubble = nil
+        }
+    }
+
+    private func handleUsageTipsHoverChange(_ hovering: Bool) {
+        usageTipsHovered = hovering
+        if hovering, appGridInteraction.hoveredBubble != nil {
             appGridInteraction.hoveredBubble = nil
         }
     }
@@ -1875,10 +2110,18 @@
 
     private func commitBubbleNote() {
         guard let context = appGridInteraction.editingBubble else { return }
+        guard appGridInteraction.proPrompt == nil else { return }
         let limited = String(appGridInteraction.bubbleDraftNote.prefix(TagDatabase.maxAppNoteLength))
             .trimmingCharacters(in: .whitespacesAndNewlines)
-        TagEditor.setAppNote(limited, for: context.app.path.path)
+        let decision = TagEditor.setAppNote(limited, for: context.app.path.path)
         appGridInteraction.bubbleDraftNote = limited
+        guard case .allow = decision else {
+            if case .blocked(let status) = decision {
+                presentProPrompt(for: .unlimitedNotes, noteQuotaStatus: status)
+            }
+            bubbleNoteFocused = false
+            return
+        }
         appGridInteraction.editingBubble = nil
         bubbleNoteFocused = false
         notifyAppNoteEditing(active: false)
@@ -1922,6 +2165,159 @@
         return app.note ?? ""
     }
 
+    private func noteQuotaStatus(for app: AppInfo) -> ProNoteQuotaStatus? {
+        guard !proEntitlement.isUnlocked else { return nil }
+        let decision = ProEntitlementPolicy.noteSaveDecision(
+            note: appGridInteraction.bubbleDraftNote,
+            for: app.path.path
+        )
+        switch decision {
+        case .allow(let status), .blocked(let status):
+            return status
+        }
+    }
+
+    private func noteQuotaHintText(for app: AppInfo) -> String? {
+        guard let status = noteQuotaStatus(for: app), !status.isUnlimited else { return nil }
+        if status.remaining <= 0 {
+            return tr("pro.notes.limitReached")
+        }
+        return tr(
+            "pro.notes.remaining",
+            replacements: [
+                "%remaining%": "\(status.remaining)",
+                "%limit%": "\(status.limit)"
+            ]
+        )
+    }
+
+    private func isNoteQuotaWarning(for app: AppInfo) -> Bool {
+        guard let status = noteQuotaStatus(for: app), !status.isUnlimited else { return false }
+        return status.remaining <= 0
+    }
+
+    private func proPromptMessage(for prompt: PendingProPrompt) -> String {
+        if prompt.feature == .unlimitedNotes,
+           let status = prompt.noteQuotaStatus,
+           !status.isUnlimited,
+           status.remaining <= 0 {
+            return tr("pro.notes.limitReached")
+        }
+        return tr(prompt.feature.promptMessageKey)
+    }
+
+    private func presentProPrompt(for feature: ProFeature, noteQuotaStatus: ProNoteQuotaStatus? = nil) {
+        proEntitlement.clearTransientOperationState()
+        withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
+            appGridInteraction.proPrompt = PendingProPrompt(
+                feature: feature,
+                noteQuotaStatus: noteQuotaStatus
+            )
+        }
+    }
+
+    private func dismissProPrompt() {
+        proEntitlement.clearTransientOperationState()
+        withAnimation(.easeOut(duration: 0.18)) {
+            appGridInteraction.proPrompt = nil
+        }
+    }
+
+    private func handleProAccessStateChange(_ newState: ProAccessState) {
+        guard newState.isUnlocked else { return }
+
+        if let preview = proEntitlement.themePreviewState {
+            appGridThemeID = preview.theme.rawValue
+            proEntitlement.stopThemePreview()
+        }
+
+        let hadPrompt = appGridInteraction.proPrompt != nil
+        if hadPrompt {
+            dismissProPrompt()
+        }
+
+        if appGridInteraction.editingBubble != nil {
+            DispatchQueue.main.async {
+                bubbleNoteFocused = true
+            }
+        }
+    }
+
+    private func handleContentAppear() {
+        refreshNotchHeight()
+        refreshAppsForOverlay()
+        guard let initialQuickSearchSource, !initialQuickSearchConsumed else { return }
+
+        initialQuickSearchConsumed = true
+        if initialQuickSearchSource == QuickSearchOpenSource.globalHidden {
+            quickSearchCloseHidesOverlay = true
+            quickSearchOnlySession = true
+        }
+        if !quickSearchVisible {
+            quickSearchVisible = true
+            quickSearchFocusToken &+= 1
+        }
+        quickSearchOpenedAt = Date()
+        refreshQuickSearchResults()
+        refreshAppsForQuickSearch()
+        NotificationCenter.default.post(
+            name: .tagLauncherQuickSearchVisibilityChanged,
+            object: nil,
+            userInfo: [
+                "active": true,
+                "hideOverlayOnClose": quickSearchCloseHidesOverlay
+            ]
+        )
+    }
+
+    private func handleOverlayDidShow(_ notification: Notification) {
+        _ = notification
+        resetTransientDragState()
+        refreshNotchHeight()
+        refreshAppsForOverlay()
+    }
+
+    private func handleOverlayDidHide(_ notification: Notification) {
+        _ = notification
+        resetTransientDragState()
+        closeQuickSearch(notify: true, hideOverlayIfNeeded: false)
+        quickSearchOnlySession = false
+        quickSearchCloseHidesOverlay = false
+    }
+
+    private func handleQuickSearchRequested(_ notification: Notification) {
+        let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay
+        openQuickSearch(source: source)
+    }
+
+    private func handleQuickSearchDismissRequested(_ notification: Notification) {
+        let source = notification.userInfo?["source"] as? String
+        closeQuickSearch(dismissalSource: source)
+    }
+
+    private func handleApplicationDidBecomeActive(_ notification: Notification) {
+        _ = notification
+        guard allApps.isEmpty, !refreshInProgress else { return }
+        refreshApps()
+    }
+
+    private func handleDataDidChange(_ notification: Notification) {
+        _ = notification
+        refreshApps(forceLayoutRefresh: true)
+    }
+
+    private func handleAppLanguageDidChange(_ notification: Notification) {
+        _ = notification
+        let appsSnapshot = allApps
+        DispatchQueue.global(qos: .userInitiated).async {
+            _ = AppleDefaultAppCatalog.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot)
+            _ = SmartStartService.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot)
+            DispatchQueue.main.async {
+                refreshApps(forceLayoutRefresh: true)
+            }
+        }
+    }
+
     private func bubblePlacement(for frame: CGRect, rootFrame: CGRect) -> BubblePlacement {
         frame.minY < 170 ? .below : .above
     }
@@ -1957,7 +2353,8 @@
 
     private func estimatedBubbleHeight(for app: AppInfo, width: CGFloat, isEditing: Bool) -> CGFloat {
         if isEditing {
-            return 120
+            let hint = noteQuotaHintText(for: app)
+            return hint == nil ? 124 : 148
         }
         let note = currentNote(for: app).trimmingCharacters(in: .whitespacesAndNewlines)
         guard !note.isEmpty else { return 68 }
@@ -1981,7 +2378,7 @@
     }
 
     private func handleTagNavigationHover(_ id: String, active: Bool) {
-        if appGridInteraction.appDragModeActive {
+        if appGridInteraction.appDragModeActive || tagNavDragModeActive {
             if !active, appGridInteraction.tagNavigationHoveredGroupName == id {
                 appGridInteraction.tagNavigationHoveredGroupName = nil
             }
@@ -1997,6 +2394,7 @@
 
         appGridInteraction.tagNavigationHoveredGroupName = id
         fillColorlessContainer(id)
+        scheduleTagNavigationHoverScroll(id)
     }
 
     private func handleTagNavigationAppDropHover(_ id: String, active: Bool) {
@@ -2015,6 +2413,26 @@
         }
 
         appGridInteraction.tagNavigationAppDropTargetName = id
+    }
+
+    private func scheduleTagNavigationHoverScroll(_ id: String) {
+        DispatchQueue.main.asyncAfter(deadline: .now() + tagNavigationHoverScrollDelay) {
+            guard appGridInteraction.tagNavigationHoveredGroupName == id else { return }
+            guard !appGridInteraction.appDragModeActive && !tagNavDragModeActive else { return }
+            scrollToTagFromHover(id)
+        }
+    }
+
+    private func scrollToTagFromHover(_ id: String) {
+        let now = Date()
+        if appGridInteraction.tagNavigationLastHoverScrollID == id,
+           let lastScrollAt = appGridInteraction.tagNavigationLastHoverScrollAt,
+           now.timeIntervalSince(lastScrollAt) < tagNavigationHoverScrollInterval {
+            return
+        }
+        appGridInteraction.tagNavigationLastHoverScrollID = id
+        appGridInteraction.tagNavigationLastHoverScrollAt = now
+        scrollTo(id)
     }
 
     private func fillColorlessContainer(_ id: String) {
@@ -2119,6 +2537,8 @@
     private func dropApp(path: String, sourceTag: String, targetTag: String, copy: Bool) {
         resetTransientDragState(keepingPendingUncategorizedDrop: true)
 
+        guard sourceTag != targetTag else { return }
+
         if isUncategorizedDropTarget(targetTag) {
             confirmAndMoveAppToUncategorized(path: path)
             return
@@ -2130,7 +2550,6 @@
         }
 
         guard tagColors[targetTag] != nil else { return }
-        guard sourceTag != targetTag || copy else { return }
         TagEditor.moveApp(
             path: path,
             from: sourceTag,
@@ -2149,6 +2568,22 @@
         guard !appHasTag(app, tagName: targetTag) else { return }
         TagEditor.appendTags([targetTag], to: [path])
         showDropRefresh()
+        refreshApps(forceLayoutRefresh: true)
+    }
+
+    private func reorderApps(inContainer containerID: String, orderedPaths: [String]) {
+        resetTransientDragState()
+        let key = TagDatabase.normalizedContainerID(containerID, tags: tagDefinitions)
+        let paths = TagDatabase.normalizedAppOrderPaths(orderedPaths)
+        guard !key.isEmpty, !paths.isEmpty else { return }
+
+        containerAppOrder[key] = paths
+        rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
+        guard proEntitlement.isUnlocked else {
+            showDropWarning(message: tr("pro.sorting.previewToast"))
+            return
+        }
+        TagEditor.reorderApps(inContainer: key, orderedPaths: paths)
         refreshApps(forceLayoutRefresh: true)
     }
 
@@ -2297,6 +2732,40 @@
         }
     }
 
+    private func requestHideUsageTips() {
+        guard !hideUsageTips else { return }
+        guard !skipUsageTipsCloseReminder else {
+            hideUsageTips = true
+            return
+        }
+        clearAppBubbleState()
+        appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
+        withAnimation(.spring(response: 0.24, dampingFraction: 0.84)) {
+            appGridInteraction.usageTipsCloseReminderVisible = true
+        }
+        publishModalInteractionState()
+    }
+
+    private func dismissUsageTipsCloseReminder() {
+        appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
+        withAnimation(.easeOut(duration: 0.18)) {
+            appGridInteraction.usageTipsCloseReminderVisible = false
+        }
+        publishModalInteractionState()
+    }
+
+    private func confirmUsageTipsCloseReminder() {
+        if appGridInteraction.usageTipsCloseSuppressFuturePrompt {
+            skipUsageTipsCloseReminder = true
+        }
+        appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
+        withAnimation(.easeOut(duration: 0.16)) {
+            appGridInteraction.usageTipsCloseReminderVisible = false
+        }
+        hideUsageTips = true
+        publishModalInteractionState()
+    }
+
     private func removeTagFromDroppedApp(app: AppInfo, tagName: String) {
         guard isRemovableRegularTag(tagName),
               appHasTag(app, tagName: tagName)
@@ -2334,9 +2803,9 @@
         return !protectedNames.contains(tag) && tagColors[tag] != nil
     }
 
-    private func showDropWarning() {
+    private func showDropWarning(message: String = tr("drop.systemDefaultWarning")) {
         withAnimation(.spring(response: 0.24, dampingFraction: 0.82)) {
-            appGridInteraction.dropWarningToast = tr("drop.systemDefaultWarning")
+            appGridInteraction.dropWarningToast = message
         }
         DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) {
             withAnimation(.easeOut(duration: 0.18)) {
@@ -2430,6 +2899,7 @@
         }
 
         refreshInProgress = true
+        scheduleLoadingSpinnerIfNeeded()
         DispatchQueue.global(qos: .userInitiated).async {
             let result = AppLibraryController.refresh(useCache: useCache)
             DispatchQueue.main.async {
@@ -2439,6 +2909,7 @@
                     finishDropRefreshAfterMinimumDuration()
                 }
                 refreshInProgress = false
+                hideLoadingSpinner()
                 if refreshAgainAfterCurrent {
                     let shouldForceLayout = refreshAgainForceLayout
                     let shouldUseCache = refreshAgainUseCache
@@ -2451,10 +2922,31 @@
         }
     }
 
+    private func scheduleLoadingSpinnerIfNeeded() {
+        loadingSpinnerToken &+= 1
+        let token = loadingSpinnerToken
+        loadingSpinnerVisible = false
+        guard allApps.isEmpty else { return }
+        DispatchQueue.main.asyncAfter(deadline: .now() + loadingSpinnerDelay) {
+            guard token == loadingSpinnerToken,
+                  refreshInProgress,
+                  allApps.isEmpty
+            else { return }
+            loadingSpinnerVisible = true
+        }
+    }
+
+    private func hideLoadingSpinner() {
+        loadingSpinnerToken &+= 1
+        loadingSpinnerVisible = false
+    }
+
     private func applyAppLibrarySnapshot(_ snapshot: AppLibrarySnapshot) {
         allApps = snapshot.apps
         quickSearchDocuments = snapshot.quickSearchDocuments
         tagColors = snapshot.tagColors
+        tagDefinitions = snapshot.tagDefinitions
+        containerAppOrder = snapshot.containerAppOrder
         draggedTagNames = snapshot.tagOrder
         rebuildDisplayGroups(apps: snapshot.apps, tagOrder: snapshot.tagOrder)
         if quickSearchVisible {
@@ -2484,6 +2976,15 @@
             closeQuickSearch(hideOverlayIfNeeded: true)
             hideOverlay()
         }
+        if isCurrentApp(app) {
+            if closeQuickSearchOnSuccess && !closeQuickSearchAndOverlayBeforeOpening {
+                closeQuickSearch(hideOverlayIfNeeded: closeOverlayOnSuccess)
+            }
+            if closeOverlayOnSuccess && !closeQuickSearchAndOverlayBeforeOpening {
+                hideOverlay()
+            }
+            return
+        }
         let configuration = NSWorkspace.OpenConfiguration()
         NSWorkspace.shared.openApplication(at: app.path, configuration: configuration) { _, error in
             DispatchQueue.main.async {
@@ -2505,6 +3006,16 @@
         }
     }
 
+    private func isCurrentApp(_ app: AppInfo) -> Bool {
+        if app.bundleIdentifier?.caseInsensitiveCompare(AppIdentity.bundleIdentifier) == .orderedSame {
+            return true
+        }
+
+        let currentURL = Bundle.main.bundleURL.standardizedFileURL.resolvingSymlinksInPath()
+        let appURL = app.path.standardizedFileURL.resolvingSymlinksInPath()
+        return currentURL == appURL
+    }
+
     func openApp(_ app: AppInfo) {
         hideOverlay()
         launchApp(app, closeOverlayOnSuccess: false)

--
Gitblit v1.9.3