From bd792cd36d57b8f0741af9a6a64e59969cdce490 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 14 Jun 2026 14:56:42 +0800
Subject: [PATCH] Prepare 7.9.1 build 20260614.1454

---
 src/Apptag/ContentView.swift |  105 ++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 81 insertions(+), 24 deletions(-)

diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index e2bf1c7..ed6f932 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -319,8 +319,6 @@
     var bubbleDraftNote = ""
     var tagNavigationHoveredGroupName: String? = nil
     var tagNavigationAppDropTargetName: String? = nil
-    var tagNavigationLastHoverScrollID: String? = nil
-    var tagNavigationLastHoverScrollAt: Date? = nil
 }
 
 private struct EditActionFeedback: Identifiable {
@@ -350,9 +348,13 @@
     @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
@@ -414,6 +416,7 @@
     @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("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation
     @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false
     @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false
@@ -423,7 +426,8 @@
     private let floatingControlsTrailingInset: CGFloat = 20
     private let floatingControlsReservedWidth: CGFloat = 120
     private var appBubbleDisabled: Bool {
-        appGridInteraction.appDragModeActive
+        usageTipsHovered
+            || appGridInteraction.appDragModeActive
             || appGridInteraction.pendingUncategorizedDrop != nil
             || appGridInteraction.pendingTagRemovalDrop != nil
     }
@@ -432,8 +436,6 @@
             ?? (isColorlessContainerMode ? filledColorlessContainer : nil)
     }
     private let rightSidebarFloatingClearance: CGFloat = 44
-    private let tagNavigationHoverScrollInterval: TimeInterval = 0.22
-
     private var floatingButtonSurfaceColor: Color {
         colorScheme == .dark
             ? Color.white.opacity(0.10)
@@ -450,6 +452,23 @@
 
     private var isColorlessContainerMode: Bool {
         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 {
@@ -623,6 +642,9 @@
         .onChange(of: appGridInteraction.pendingTagRemovalDrop != nil) { _, _ in
             publishModalInteractionState()
         }
+        .onChange(of: smartStartNotice != nil) { _, _ in
+            publishModalInteractionState()
+        }
         .onDisappear {
             NotificationCenter.default.post(
                 name: .tagLauncherModalInteractionChanged,
@@ -636,7 +658,11 @@
         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
+            ]
         )
     }
 
@@ -1116,6 +1142,10 @@
                     bubbleDisabled: appBubbleDisabled,
                     showUncommonAppBubbles: showUncommonAppBubbles,
                     highlightedGroupName: appGridHighlightedGroupName,
+                    bottomContentPadding: shouldShowUsageTips ? AppGridUsageTipsMetrics.reservedHeight : 0,
+                    usageTipsVisible: shouldShowUsageTips,
+                    usageTips: appGridUsageTips,
+                    selectedUsageTipIndex: $selectedUsageTipIndex,
                     contentRevision: groupLayoutVersion,
                     scrollTargetID: appGridScrollTargetID,
                     scrollRequestToken: appGridScrollRequestToken,
@@ -1128,13 +1158,17 @@
                     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) },
+                    onUsageTipsHoverChange: handleUsageTipsHoverChange
                 )
             }
         }
@@ -1775,13 +1809,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
         }
@@ -1840,6 +1888,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
         }
     }
@@ -2001,7 +2056,6 @@
 
         appGridInteraction.tagNavigationHoveredGroupName = id
         fillColorlessContainer(id)
-        scrollToTagFromHover(id)
     }
 
     private func handleTagNavigationAppDropHover(_ id: String, active: Bool) {
@@ -2020,18 +2074,6 @@
         }
 
         appGridInteraction.tagNavigationAppDropTargetName = 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) {
@@ -2136,6 +2178,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
@@ -2147,7 +2191,6 @@
         }
 
         guard tagColors[targetTag] != nil else { return }
-        guard sourceTag != targetTag || copy else { return }
         TagEditor.moveApp(
             path: path,
             from: sourceTag,
@@ -2166,6 +2209,18 @@
         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)
+        TagEditor.reorderApps(inContainer: key, orderedPaths: paths)
         refreshApps(forceLayoutRefresh: true)
     }
 
@@ -2472,6 +2527,8 @@
         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 {

--
Gitblit v1.9.3