From ab59aa3a800139e2fe80f7e802dfada2258ab0f6 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 25 Jun 2026 16:24:01 +0800
Subject: [PATCH] Add usage tips close reminder

---
 src/Apptag/ContentView.swift |  200 +++++++++++++++++++++++++++++++++++--------------
 1 files changed, 141 insertions(+), 59 deletions(-)

diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index 9f0743c..7ab2869 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -328,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
@@ -438,6 +440,7 @@
     @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
@@ -449,6 +452,7 @@
             || appGridInteraction.appDragModeActive
             || appGridInteraction.pendingUncategorizedDrop != nil
             || appGridInteraction.pendingTagRemovalDrop != nil
+            || appGridInteraction.usageTipsCloseReminderVisible
     }
     private var appGridHighlightedGroupName: String? {
         appGridInteraction.tagNavigationHoveredGroupName
@@ -514,68 +518,13 @@
             }
 
             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()
@@ -681,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,
@@ -689,6 +708,7 @@
                 "active": smartStartNotice != nil
                     || appGridInteraction.pendingUncategorizedDrop != nil
                     || appGridInteraction.pendingTagRemovalDrop != nil
+                    || appGridInteraction.usageTipsCloseReminderVisible
             ]
         )
     }
@@ -1242,7 +1262,7 @@
                     },
                     onScrollActivity: handleAppGridScrollActivity,
                     onDragModeChange: { setAppDragMode($0) },
-                    onHideUsageTips: { hideUsageTips = true },
+                    onHideUsageTips: requestHideUsageTips,
                     onUsageTipsHoverChange: handleUsageTipsHoverChange
                 )
             }
@@ -1689,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 {
@@ -2496,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)

--
Gitblit v1.9.3