From c1d6cfc2b59b2ce4f1ebf235397cdf43a4292996 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 24 Jun 2026 20:41:45 +0800
Subject: [PATCH] Release 8.0.1 dark glass themes

---
 src/Apptag/ContentView.swift |  389 ++++++++++++++++++++++++++++++-------------------------
 1 files changed, 212 insertions(+), 177 deletions(-)

diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index 122addf..87ffec3 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -8,9 +8,15 @@
     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 tags = "tags"
 }
 
 // MARK: - Edit Phase
@@ -82,6 +88,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 +116,9 @@
         }
 
         func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
+            if textView.hasMarkedText() {
+                return false
+            }
             if commandSelector == #selector(NSResponder.insertNewline(_:)) {
                 onSubmit?()
                 return true
@@ -319,6 +332,8 @@
     var bubbleDraftNote = ""
     var tagNavigationHoveredGroupName: String? = nil
     var tagNavigationAppDropTargetName: String? = nil
+    var tagNavigationLastHoverScrollID: String? = nil
+    var tagNavigationLastHoverScrollAt: Date? = nil
 }
 
 private struct EditActionFeedback: Identifiable {
@@ -340,120 +355,6 @@
     let tagName: String
 }
 
-private struct AppGridUsageTip: Identifiable {
-    let id: Int
-    let titleKey: String
-    let detailKey: String
-}
-
-private struct AppGridUsageTipsBar: View {
-    static let reservedHeight: CGFloat = 64
-
-    let tips: [AppGridUsageTip]
-    @Binding var selectedIndex: Int
-    let dragModeActive: Bool
-
-    @State private var textHovered = false
-
-    private var safeIndex: Int {
-        guard tips.indices.contains(selectedIndex) else { return 0 }
-        return selectedIndex
-    }
-
-    private var currentTip: AppGridUsageTip? {
-        guard !tips.isEmpty else { return nil }
-        return tips[safeIndex]
-    }
-
-    var body: some View {
-        if let tip = currentTip {
-            HStack(spacing: 0) {
-                Text(tr(tip.titleKey))
-                    .font(.system(size: 14, weight: .semibold))
-                    .foregroundStyle(Color.primary)
-                    .lineLimit(1)
-                    .minimumScaleFactor(0.82)
-                    .padding(.horizontal, 14)
-                    .frame(width: 174, height: 42, alignment: .leading)
-                    .background(Color(nsColor: .controlBackgroundColor).opacity(0.96))
-
-                tipDetailText(tip)
-                    .frame(height: 42)
-                    .frame(maxWidth: .infinity, alignment: .leading)
-                    .background(Color.black.opacity(0.88))
-
-                tipsControls
-                    .frame(width: 86, height: 42)
-                    .background(Color.black.opacity(0.88))
-            }
-            .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
-            .overlay(
-                RoundedRectangle(cornerRadius: 8, style: .continuous)
-                    .stroke(Color.primary.opacity(0.18), lineWidth: 1)
-            )
-            .shadow(color: .black.opacity(0.20), radius: 14, y: 7)
-            .frame(maxWidth: 980)
-            .padding(.horizontal, 18)
-            .padding(.bottom, 12)
-            .opacity(dragModeActive ? 0.72 : 1.0)
-            .allowsHitTesting(!dragModeActive)
-            .accessibilityElement(children: .combine)
-            .accessibilityLabel("\(tr(tip.titleKey)) \(tr(tip.detailKey))")
-        }
-    }
-
-    private func tipDetailText(_ tip: AppGridUsageTip) -> some View {
-        ScrollView(.horizontal, showsIndicators: textHovered) {
-            Text(tr(tip.detailKey))
-                .font(.system(size: 15, weight: .medium))
-                .foregroundStyle(Color.white.opacity(0.96))
-                .lineLimit(1)
-                .fixedSize(horizontal: true, vertical: false)
-                .padding(.horizontal, 14)
-                .frame(minHeight: 42, alignment: .center)
-        }
-        .onHover { textHovered = $0 }
-        .help(tr(tip.detailKey))
-    }
-
-    private var tipsControls: some View {
-        VStack(spacing: 4) {
-            HStack(spacing: 8) {
-                usageTipNavigationButton(systemImage: "chevron.left", labelKey: "usageTips.previous") {
-                    selectedIndex = (safeIndex - 1 + tips.count) % tips.count
-                }
-                usageTipNavigationButton(systemImage: "chevron.right", labelKey: "usageTips.next") {
-                    selectedIndex = (safeIndex + 1) % tips.count
-                }
-            }
-
-            HStack(spacing: 3) {
-                ForEach(tips.indices, id: \.self) { index in
-                    Circle()
-                        .fill(index == safeIndex ? Color.accentColor : Color.white.opacity(0.58))
-                        .frame(width: 4, height: 4)
-                }
-            }
-        }
-    }
-
-    private func usageTipNavigationButton(
-        systemImage: String,
-        labelKey: String,
-        action: @escaping () -> Void
-    ) -> some View {
-        Button(action: action) {
-            Image(systemName: systemImage)
-                .font(.system(size: 15, weight: .bold))
-                .foregroundStyle(Color.white.opacity(0.96))
-                .frame(width: 24, height: 22)
-                .contentShape(Rectangle())
-        }
-        .buttonStyle(.plain)
-        .accessibilityLabel(tr(labelKey))
-    }
-}
-
 struct ContentView: View {
     let hideOverlay: () -> Void
     private let initialQuickSearchSource: String?
@@ -468,6 +369,7 @@
     @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
@@ -489,6 +391,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
@@ -530,6 +434,7 @@
     @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("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation
     @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false
     @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false
@@ -538,8 +443,10 @@
     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
     }
@@ -548,10 +455,20 @@
             ?? (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 ? appGridTheme : .defaultLight
     }
 
     private var isSideLayout: Bool {
@@ -593,9 +510,7 @@
     var body: some View {
         ZStack {
             if shouldRenderAppGridBehindQuickSearch {
-                VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
-                    .ignoresSafeArea()
-                    .allowsHitTesting(false)
+                appGridBackground
             }
 
             if shouldRenderAppGridBehindQuickSearch {
@@ -1057,6 +972,39 @@
         }
     }
 
+    private var appGridBackground: some View {
+        let theme = renderedAppGridTheme
+        return ZStack {
+            if theme.usesVisualEffectBackdrop {
+                VisualEffectView(
+                    material: theme.material,
+                    blendingMode: .behindWindow
+                )
+            }
+            if theme.isPureBlackBackground {
+                Color.black
+            }
+            if !theme.isDefaultLight {
+                LinearGradient(
+                    colors: theme.backgroundBaseColors,
+                    startPoint: .topLeading,
+                    endPoint: .bottomTrailing
+                )
+                LinearGradient(
+                    colors: theme.backgroundAccentColors,
+                    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") {
@@ -1154,6 +1102,9 @@
             onActivate: { tagID in
                 activateTagNavigation(tagID)
             },
+            onDoubleActivate: { _ in
+                openTagSettingsFromNavigation()
+            },
             onHoverChange: { tagID, active in
                 handleTagNavigationHover(tagID, active: active)
             },
@@ -1178,6 +1129,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]
         )
     }
 
@@ -1242,53 +1201,49 @@
         Group {
             if allApps.isEmpty {
                 Spacer()
-                ProgressView().scaleEffect(0.8)
+                if loadingSpinnerVisible {
+                    ProgressView().scaleEffect(0.8)
+                }
                 Spacer()
             } else {
-                ZStack(alignment: .bottom) {
-                    AppGridCollectionView(
-                        groups: displayGroups,
-                        tagColors: tagColors,
-                        displayMode: displayMode,
-                        iconSize: iconSize,
-                        showNames: !hideAppNames,
-                        bubbleDisabled: appBubbleDisabled,
-                        showUncommonAppBubbles: showUncommonAppBubbles,
-                        highlightedGroupName: appGridHighlightedGroupName,
-                        bottomContentPadding: shouldShowUsageTips ? AppGridUsageTipsBar.reservedHeight : 0,
-                        contentRevision: groupLayoutVersion,
-                        scrollTargetID: appGridScrollTargetID,
-                        scrollRequestToken: appGridScrollRequestToken,
-                        onSelectApp: { app in openApp(app) },
-                        onBubbleHover: handleBubbleHover,
-                        onEditNote: beginEditingBubbleNote,
-                        onDropApp: { path, source, target, copy in
-                            dropApp(path: path, sourceTag: source, targetTag: target, copy: copy)
-                        },
-                        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) }
-                    )
-
-                    if shouldShowUsageTips {
-                        AppGridUsageTipsBar(
-                            tips: appGridUsageTips,
-                            selectedIndex: $selectedUsageTipIndex,
-                            dragModeActive: appGridInteraction.appDragModeActive
-                        )
-                        .zIndex(3)
-                    }
-                }
+                AppGridCollectionView(
+                    groups: displayGroups,
+                    tagColors: tagColors,
+                    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,
+                    onSelectApp: { app in openApp(app) },
+                    onBubbleHover: handleBubbleHover,
+                    onEditNote: beginEditingBubbleNote,
+                    onDropApp: { path, source, target, copy in
+                        dropApp(path: path, sourceTag: source, targetTag: target, copy: copy)
+                    },
+                    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) },
+                    onUsageTipsHoverChange: handleUsageTipsHoverChange
+                )
             }
         }
     }
@@ -1349,17 +1304,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,
@@ -1374,6 +1344,7 @@
     private var editAppsView: some View {
         VStack(spacing: 0) {
             EditAppsHeaderView(
+                theme: renderedAppGridTheme,
                 operation: editTagOperation,
                 hintText: editModeHintText,
                 confirmTitle: editConfirmTitle,
@@ -1387,11 +1358,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
                     )
@@ -1423,10 +1395,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
                 }
@@ -1450,12 +1426,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)
 
@@ -1488,7 +1464,8 @@
             colorIndex: colorIndex,
             operation: editTagOperation,
             isSelected: isSelected,
-            isRemovableCandidate: isRemovableCandidate
+            isRemovableCandidate: isRemovableCandidate,
+            theme: renderedAppGridTheme
         ) {
             if isSelected {
                 selectedTagNames.remove(tagName)
@@ -1562,7 +1539,8 @@
         return EditableAppSelectionItem(
             app: app,
             iconSize: iconSize,
-            isSelected: isSelected
+            isSelected: isSelected,
+            theme: renderedAppGridTheme
         ) {
             toggleEditableAppSelection(app)
         }
@@ -1985,16 +1963,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() {
@@ -2007,6 +1993,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
         }
     }
@@ -2152,7 +2145,7 @@
     }
 
     private func handleTagNavigationHover(_ id: String, active: Bool) {
-        if appGridInteraction.appDragModeActive {
+        if appGridInteraction.appDragModeActive || tagNavDragModeActive {
             if !active, appGridInteraction.tagNavigationHoveredGroupName == id {
                 appGridInteraction.tagNavigationHoveredGroupName = nil
             }
@@ -2168,6 +2161,7 @@
 
         appGridInteraction.tagNavigationHoveredGroupName = id
         fillColorlessContainer(id)
+        scheduleTagNavigationHoverScroll(id)
     }
 
     private func handleTagNavigationAppDropHover(_ id: String, active: Bool) {
@@ -2186,6 +2180,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) {
@@ -2614,6 +2628,7 @@
         }
 
         refreshInProgress = true
+        scheduleLoadingSpinnerIfNeeded()
         DispatchQueue.global(qos: .userInitiated).async {
             let result = AppLibraryController.refresh(useCache: useCache)
             DispatchQueue.main.async {
@@ -2623,6 +2638,7 @@
                     finishDropRefreshAfterMinimumDuration()
                 }
                 refreshInProgress = false
+                hideLoadingSpinner()
                 if refreshAgainAfterCurrent {
                     let shouldForceLayout = refreshAgainForceLayout
                     let shouldUseCache = refreshAgainUseCache
@@ -2635,6 +2651,25 @@
         }
     }
 
+    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

--
Gitblit v1.9.3