From 77896f65b92c756da699e84499b180d35b0f8835 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 25 Jun 2026 18:52:06 +0800
Subject: [PATCH] Merge dark glass 8.0.5 freeze

---
 src/Apptag/ContentView.swift |  348 +++++++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 273 insertions(+), 75 deletions(-)

diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index 745a918..7ab2869 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
@@ -322,6 +328,8 @@
     var pendingTagRemovalDrop: PendingTagRemovalDrop? = nil
     var uncategorizedDropSuppressFuturePrompt = false
     var tagRemovalDropSuppressFuturePrompt = false
+    var usageTipsCloseReminderVisible = false
+    var usageTipsCloseSuppressFuturePrompt = false
     var appDragResetToken = 0
     var bubbleDraftNote = ""
     var tagNavigationHoveredGroupName: String? = nil
@@ -385,6 +393,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
@@ -426,19 +436,23 @@
     @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
+    @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 {
         usageTipsHovered
             || appGridInteraction.appDragModeActive
             || appGridInteraction.pendingUncategorizedDrop != nil
             || appGridInteraction.pendingTagRemovalDrop != nil
+            || appGridInteraction.usageTipsCloseReminderVisible
     }
     private var appGridHighlightedGroupName: String? {
         appGridInteraction.tagNavigationHoveredGroupName
@@ -448,9 +462,17 @@
     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 {
@@ -492,74 +514,17 @@
     var body: 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()
@@ -665,6 +630,76 @@
         }
     }
 
+    @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
+    }
+
+    @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 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)
+        }
+    }
+
     private func publishModalInteractionState() {
         NotificationCenter.default.post(
             name: .tagLauncherModalInteractionChanged,
@@ -673,6 +708,7 @@
                 "active": smartStartNotice != nil
                     || appGridInteraction.pendingUncategorizedDrop != nil
                     || appGridInteraction.pendingTagRemovalDrop != nil
+                    || appGridInteraction.usageTipsCloseReminderVisible
             ]
         )
     }
@@ -956,6 +992,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") {
@@ -1053,6 +1122,9 @@
             onActivate: { tagID in
                 activateTagNavigation(tagID)
             },
+            onDoubleActivate: { _ in
+                openTagSettingsFromNavigation()
+            },
             onHoverChange: { tagID, active in
                 handleTagNavigationHover(tagID, active: active)
             },
@@ -1077,6 +1149,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]
         )
     }
 
@@ -1141,7 +1221,9 @@
         Group {
             if allApps.isEmpty {
                 Spacer()
-                ProgressView().scaleEffect(0.8)
+                if loadingSpinnerVisible {
+                    ProgressView().scaleEffect(0.8)
+                }
                 Spacer()
             } else {
                 AppGridCollectionView(
@@ -1150,6 +1232,7 @@
                     displayMode: displayMode,
                     iconSize: iconSize,
                     showNames: !hideAppNames,
+                    appGridTheme: renderedAppGridTheme,
                     bubbleDisabled: appBubbleDisabled,
                     showUncommonAppBubbles: showUncommonAppBubbles,
                     highlightedGroupName: appGridHighlightedGroupName,
@@ -1179,6 +1262,7 @@
                     },
                     onScrollActivity: handleAppGridScrollActivity,
                     onDragModeChange: { setAppDragMode($0) },
+                    onHideUsageTips: requestHideUsageTips,
                     onUsageTipsHoverChange: handleUsageTipsHoverChange
                 )
             }
@@ -1241,17 +1325,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,
@@ -1266,6 +1365,7 @@
     private var editAppsView: some View {
         VStack(spacing: 0) {
             EditAppsHeaderView(
+                theme: renderedAppGridTheme,
                 operation: editTagOperation,
                 hintText: editModeHintText,
                 confirmTitle: editConfirmTitle,
@@ -1279,11 +1379,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
                     )
@@ -1315,10 +1416,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
                 }
@@ -1342,12 +1447,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)
 
@@ -1380,7 +1485,8 @@
             colorIndex: colorIndex,
             operation: editTagOperation,
             isSelected: isSelected,
-            isRemovableCandidate: isRemovableCandidate
+            isRemovableCandidate: isRemovableCandidate,
+            theme: renderedAppGridTheme
         ) {
             if isSelected {
                 selectedTagNames.remove(tagName)
@@ -1454,7 +1560,8 @@
         return EditableAppSelectionItem(
             app: app,
             iconSize: iconSize,
-            isSelected: isSelected
+            isSelected: isSelected,
+            theme: renderedAppGridTheme
         ) {
             toggleEditableAppSelection(app)
         }
@@ -1602,6 +1709,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 {
@@ -1877,16 +2012,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() {
@@ -2401,6 +2544,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)
@@ -2534,6 +2711,7 @@
         }
 
         refreshInProgress = true
+        scheduleLoadingSpinnerIfNeeded()
         DispatchQueue.global(qos: .userInitiated).async {
             let result = AppLibraryController.refresh(useCache: useCache)
             DispatchQueue.main.async {
@@ -2543,6 +2721,7 @@
                     finishDropRefreshAfterMinimumDuration()
                 }
                 refreshInProgress = false
+                hideLoadingSpinner()
                 if refreshAgainAfterCurrent {
                     let shouldForceLayout = refreshAgainForceLayout
                     let shouldUseCache = refreshAgainUseCache
@@ -2555,6 +2734,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